You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ApplicationRunnerServlet.java 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.launcher;
  17. import java.io.File;
  18. import java.io.IOException;
  19. import java.io.Serializable;
  20. import java.lang.reflect.InvocationHandler;
  21. import java.lang.reflect.Method;
  22. import java.lang.reflect.Proxy;
  23. import java.net.MalformedURLException;
  24. import java.net.URI;
  25. import java.net.URISyntaxException;
  26. import java.net.URL;
  27. import java.util.Arrays;
  28. import java.util.Collections;
  29. import java.util.LinkedHashSet;
  30. import java.util.LinkedList;
  31. import java.util.Map;
  32. import java.util.Properties;
  33. import java.util.logging.Level;
  34. import java.util.logging.Logger;
  35. import javax.servlet.ServletConfig;
  36. import javax.servlet.ServletException;
  37. import javax.servlet.http.HttpServletRequest;
  38. import javax.servlet.http.HttpServletResponse;
  39. import javax.servlet.http.HttpSession;
  40. import com.vaadin.launcher.CustomDeploymentConfiguration.Conf;
  41. import com.vaadin.server.DefaultDeploymentConfiguration;
  42. import com.vaadin.server.DeploymentConfiguration;
  43. import com.vaadin.server.LegacyApplication;
  44. import com.vaadin.server.LegacyVaadinServlet;
  45. import com.vaadin.server.ServiceException;
  46. import com.vaadin.server.SystemMessages;
  47. import com.vaadin.server.SystemMessagesInfo;
  48. import com.vaadin.server.SystemMessagesProvider;
  49. import com.vaadin.server.UIClassSelectionEvent;
  50. import com.vaadin.server.UICreateEvent;
  51. import com.vaadin.server.UIProvider;
  52. import com.vaadin.server.VaadinRequest;
  53. import com.vaadin.server.VaadinService;
  54. import com.vaadin.server.VaadinServlet;
  55. import com.vaadin.server.VaadinServletRequest;
  56. import com.vaadin.server.VaadinServletService;
  57. import com.vaadin.server.VaadinSession;
  58. import com.vaadin.shared.ApplicationConstants;
  59. import com.vaadin.tests.components.TestBase;
  60. import com.vaadin.ui.UI;
  61. import com.vaadin.util.CurrentInstance;
  62. @SuppressWarnings("serial")
  63. public class ApplicationRunnerServlet extends LegacyVaadinServlet {
  64. private static class ApplicationRunnerRedirectException
  65. extends RuntimeException {
  66. private final String target;
  67. public ApplicationRunnerRedirectException(String target) {
  68. this.target = target;
  69. }
  70. public String getTarget() {
  71. return target;
  72. }
  73. public static String extractRedirectTarget(ServletException e) {
  74. Throwable cause = e.getCause();
  75. if (cause instanceof ServiceException) {
  76. ServiceException se = (ServiceException) cause;
  77. Throwable serviceCause = se.getCause();
  78. if (serviceCause instanceof ApplicationRunnerRedirectException) {
  79. ApplicationRunnerRedirectException redirect = (ApplicationRunnerRedirectException) serviceCause;
  80. return redirect.getTarget();
  81. }
  82. }
  83. return null;
  84. }
  85. }
  86. public static String CUSTOM_SYSTEM_MESSAGES_PROPERTY = "custom-"
  87. + SystemMessages.class.getName();
  88. /**
  89. * The name of the application class currently used. Only valid within one
  90. * request.
  91. */
  92. private LinkedHashSet<String> defaultPackages = new LinkedHashSet<>();
  93. private transient final ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
  94. @Override
  95. public void init(ServletConfig servletConfig) throws ServletException {
  96. super.init(servletConfig);
  97. String initParameter = servletConfig
  98. .getInitParameter("defaultPackages");
  99. if (initParameter != null) {
  100. Collections.addAll(defaultPackages, initParameter.split(","));
  101. }
  102. String str = TestBase.class.getName().replace('.', '/') + ".class";
  103. URL url = getService().getClassLoader().getResource(str);
  104. if ("file".equals(url.getProtocol())) {
  105. String path = url.getPath();
  106. try {
  107. path = new URI(path).getPath();
  108. } catch (URISyntaxException e) {
  109. getLogger().log(Level.FINE, "Failed to decode url", e);
  110. }
  111. File comVaadinTests = new File(path).getParentFile()
  112. .getParentFile();
  113. addDirectories(comVaadinTests, defaultPackages, "com.vaadin.tests");
  114. }
  115. }
  116. @Override
  117. protected void servletInitialized() throws ServletException {
  118. super.servletInitialized();
  119. getService().addSessionInitListener(
  120. event -> onVaadinSessionStarted(event.getRequest(), event.getSession()));
  121. }
  122. private void addDirectories(File parent, LinkedHashSet<String> packages,
  123. String parentPackage) {
  124. packages.add(parentPackage);
  125. for (File f : parent.listFiles()) {
  126. if (f.isDirectory()) {
  127. String newPackage = parentPackage + "." + f.getName();
  128. addDirectories(f, packages, newPackage);
  129. }
  130. }
  131. }
  132. @Override
  133. protected void service(HttpServletRequest request,
  134. HttpServletResponse response) throws ServletException, IOException {
  135. this.request.set(request);
  136. try {
  137. super.service(request, response);
  138. } catch (ServletException e) {
  139. String redirectTarget = ApplicationRunnerRedirectException
  140. .extractRedirectTarget(e);
  141. if (redirectTarget != null) {
  142. response.sendRedirect(redirectTarget + "?restartApplication");
  143. } else {
  144. // Not the exception we were looking for, just rethrow
  145. throw e;
  146. }
  147. } finally {
  148. this.request.set(null);
  149. }
  150. }
  151. @Override
  152. protected URL getApplicationUrl(HttpServletRequest request)
  153. throws MalformedURLException {
  154. URL url = super.getApplicationUrl(request);
  155. String path = url.toString();
  156. path += getApplicationRunnerApplicationClassName(request);
  157. path += "/";
  158. return new URL(path);
  159. }
  160. @Override
  161. protected Class<? extends LegacyApplication> getApplicationClass()
  162. throws ClassNotFoundException {
  163. return getClassToRun().asSubclass(LegacyApplication.class);
  164. }
  165. @Override
  166. protected boolean shouldCreateApplication(HttpServletRequest request)
  167. throws ServletException {
  168. try {
  169. return LegacyApplication.class.isAssignableFrom(getClassToRun());
  170. } catch (ClassNotFoundException e) {
  171. throw new ServletException(e);
  172. }
  173. }
  174. protected void onVaadinSessionStarted(VaadinRequest request,
  175. VaadinSession session) throws ServiceException {
  176. try {
  177. final Class<?> classToRun = getClassToRun();
  178. if (UI.class.isAssignableFrom(classToRun)) {
  179. session.addUIProvider(
  180. new ApplicationRunnerUIProvider(classToRun));
  181. } else if (LegacyApplication.class.isAssignableFrom(classToRun)) {
  182. // Avoid using own UIProvider for legacy Application
  183. } else if (UIProvider.class.isAssignableFrom(classToRun)) {
  184. session.addUIProvider((UIProvider) classToRun.newInstance());
  185. } else {
  186. throw new ServiceException(classToRun.getCanonicalName()
  187. + " is neither an Application nor a UI");
  188. }
  189. } catch (final IllegalAccessException e) {
  190. throw new ServiceException(e);
  191. } catch (final InstantiationException e) {
  192. throw new ServiceException(e);
  193. } catch (final ClassNotFoundException e) {
  194. throw new ServiceException(new InstantiationException(
  195. "Failed to load application class: "
  196. + getApplicationRunnerApplicationClassName(
  197. (VaadinServletRequest) request)));
  198. }
  199. }
  200. private String getApplicationRunnerApplicationClassName(
  201. HttpServletRequest request) {
  202. return getApplicationRunnerURIs(request).applicationClassname;
  203. }
  204. private static final class ProxyDeploymentConfiguration
  205. implements InvocationHandler, Serializable {
  206. private final DeploymentConfiguration originalConfiguration;
  207. private ProxyDeploymentConfiguration(
  208. DeploymentConfiguration originalConfiguration) {
  209. this.originalConfiguration = originalConfiguration;
  210. }
  211. @Override
  212. public Object invoke(Object proxy, Method method, Object[] args)
  213. throws Throwable {
  214. if (method.getDeclaringClass() == DeploymentConfiguration.class) {
  215. // Find the configuration instance to delegate to
  216. DeploymentConfiguration configuration = findDeploymentConfiguration(
  217. originalConfiguration);
  218. return method.invoke(configuration, args);
  219. } else {
  220. return method.invoke(proxy, args);
  221. }
  222. }
  223. }
  224. private static final class ApplicationRunnerUIProvider extends UIProvider {
  225. private final Class<?> classToRun;
  226. private ApplicationRunnerUIProvider(Class<?> classToRun) {
  227. this.classToRun = classToRun;
  228. }
  229. @Override
  230. public Class<? extends UI> getUIClass(UIClassSelectionEvent event) {
  231. return (Class<? extends UI>) classToRun;
  232. }
  233. @Override
  234. public UI createInstance(UICreateEvent event) {
  235. event.getRequest().setAttribute(ApplicationConstants.UI_ROOT_PATH,
  236. "/" + event.getUIClass().getName());
  237. return super.createInstance(event);
  238. }
  239. }
  240. // TODO Don't need to use a data object now that there's only one field
  241. private static class URIS {
  242. // String staticFilesPath;
  243. // String applicationURI;
  244. // String context;
  245. // String runner;
  246. String applicationClassname;
  247. }
  248. /**
  249. * Parses application runner URIs.
  250. *
  251. * If request URL is e.g.
  252. * http://localhost:8080/vaadin/run/com.vaadin.demo.Calc then
  253. * <ul>
  254. * <li>context=vaadin</li>
  255. * <li>Runner servlet=run</li>
  256. * <li>Vaadin application=com.vaadin.demo.Calc</li>
  257. * </ul>
  258. *
  259. * @param request
  260. * @return string array containing widgetset URI, application URI and
  261. * context, runner, application classname
  262. */
  263. private static URIS getApplicationRunnerURIs(HttpServletRequest request) {
  264. final String[] urlParts = request.getRequestURI().split("\\/");
  265. // String runner = null;
  266. URIS uris = new URIS();
  267. String applicationClassname = null;
  268. String contextPath = request.getContextPath();
  269. if (urlParts[1].equals(contextPath.replaceAll("\\/", ""))) {
  270. // class name comes after web context and runner application
  271. // runner = urlParts[2];
  272. if (urlParts.length == 3) {
  273. throw new ApplicationRunnerRedirectException(
  274. findLastModifiedApplication());
  275. } else {
  276. applicationClassname = urlParts[3];
  277. }
  278. // uris.applicationURI = "/" + context + "/" + runner + "/"
  279. // + applicationClassname;
  280. // uris.context = context;
  281. // uris.runner = runner;
  282. uris.applicationClassname = applicationClassname;
  283. } else {
  284. // no context
  285. // runner = urlParts[1];
  286. if (urlParts.length == 2) {
  287. throw new ApplicationRunnerRedirectException(
  288. findLastModifiedApplication());
  289. } else {
  290. applicationClassname = urlParts[2];
  291. }
  292. // uris.applicationURI = "/" + runner + "/" + applicationClassname;
  293. // uris.context = context;
  294. // uris.runner = runner;
  295. uris.applicationClassname = applicationClassname;
  296. }
  297. return uris;
  298. }
  299. private static String findLastModifiedApplication() {
  300. String lastModifiedClassName = null;
  301. File uitestDir = new File("src/main/java");
  302. if (uitestDir.isDirectory()) {
  303. LinkedList<File> stack = new LinkedList<>();
  304. stack.add(uitestDir);
  305. long lastModifiedTimestamp = Long.MIN_VALUE;
  306. while (!stack.isEmpty()) {
  307. File file = stack.pop();
  308. if (file.isDirectory()) {
  309. stack.addAll(Arrays.asList(file.listFiles()));
  310. } else if (file.getName().endsWith(".java")) {
  311. if (lastModifiedTimestamp < file.lastModified()) {
  312. String className = file.getPath()
  313. .substring(uitestDir.getPath().length() + 1)
  314. .replace(File.separatorChar, '.');
  315. className = className.substring(0,
  316. className.length() - ".java".length());
  317. if (isSupportedClass(className)) {
  318. lastModifiedTimestamp = file.lastModified();
  319. lastModifiedClassName = className;
  320. }
  321. }
  322. }
  323. }
  324. }
  325. if (lastModifiedClassName == null) {
  326. throw new IllegalArgumentException("No application specified");
  327. } else {
  328. return lastModifiedClassName;
  329. }
  330. }
  331. private static boolean isSupportedClass(String className) {
  332. try {
  333. Class<?> type = Class.forName(className, false,
  334. ApplicationRunnerServlet.class.getClassLoader());
  335. if (UI.class.isAssignableFrom(type)) {
  336. return true;
  337. } else if (LegacyApplication.class.isAssignableFrom(type)) {
  338. return true;
  339. } else if (UIProvider.class.isAssignableFrom(type)) {
  340. return true;
  341. }
  342. return false;
  343. } catch (Exception e) {
  344. return false;
  345. }
  346. }
  347. private Class<?> getClassToRun() throws ClassNotFoundException {
  348. // TODO use getClassLoader() ?
  349. Class<?> appClass = null;
  350. String baseName = getApplicationRunnerApplicationClassName(
  351. request.get());
  352. try {
  353. appClass = getClass().getClassLoader().loadClass(baseName);
  354. return appClass;
  355. } catch (Exception e) {
  356. //
  357. for (String pkg : defaultPackages) {
  358. try {
  359. appClass = getClass().getClassLoader()
  360. .loadClass(pkg + "." + baseName);
  361. } catch (ClassNotFoundException ee) {
  362. // Ignore as this is expected for many packages
  363. } catch (Exception e2) {
  364. // TODO: handle exception
  365. getLogger().log(Level.FINE,
  366. "Failed to find application class " + pkg + "."
  367. + baseName,
  368. e2);
  369. }
  370. if (appClass != null) {
  371. return appClass;
  372. }
  373. }
  374. }
  375. throw new ClassNotFoundException(baseName);
  376. }
  377. private Logger getLogger() {
  378. return Logger.getLogger(ApplicationRunnerServlet.class.getName());
  379. }
  380. @Override
  381. protected DeploymentConfiguration createDeploymentConfiguration(
  382. Properties initParameters) {
  383. // Get the original configuration from the super class
  384. final DeploymentConfiguration originalConfiguration = super.createDeploymentConfiguration(
  385. initParameters);
  386. // And then create a proxy instance that delegates to the original
  387. // configuration or a customized version
  388. return (DeploymentConfiguration) Proxy.newProxyInstance(
  389. DeploymentConfiguration.class.getClassLoader(),
  390. new Class[] { DeploymentConfiguration.class },
  391. new ProxyDeploymentConfiguration(originalConfiguration));
  392. }
  393. @Override
  394. protected VaadinServletService createServletService(
  395. DeploymentConfiguration deploymentConfiguration)
  396. throws ServiceException {
  397. VaadinServletService service = super.createServletService(
  398. deploymentConfiguration);
  399. final SystemMessagesProvider provider = service
  400. .getSystemMessagesProvider();
  401. service.setSystemMessagesProvider(new SystemMessagesProvider() {
  402. @Override
  403. public SystemMessages getSystemMessages(
  404. SystemMessagesInfo systemMessagesInfo) {
  405. if (systemMessagesInfo.getRequest() == null) {
  406. return provider.getSystemMessages(systemMessagesInfo);
  407. }
  408. Object messages = systemMessagesInfo.getRequest()
  409. .getAttribute(CUSTOM_SYSTEM_MESSAGES_PROPERTY);
  410. if (messages instanceof SystemMessages) {
  411. return (SystemMessages) messages;
  412. }
  413. return provider.getSystemMessages(systemMessagesInfo);
  414. }
  415. });
  416. return service;
  417. }
  418. private static DeploymentConfiguration findDeploymentConfiguration(
  419. DeploymentConfiguration originalConfiguration) throws Exception {
  420. // First level of cache
  421. DeploymentConfiguration configuration = CurrentInstance
  422. .get(DeploymentConfiguration.class);
  423. if (configuration == null) {
  424. // Not in cache, try to find a VaadinSession to get it from
  425. VaadinSession session = VaadinSession.getCurrent();
  426. if (session == null) {
  427. /*
  428. * There's no current session, request or response when serving
  429. * static resources, but there's still the current request
  430. * maintained by AppliationRunnerServlet, and there's most
  431. * likely also a HttpSession containing a VaadinSession for that
  432. * request.
  433. */
  434. HttpServletRequest currentRequest = VaadinServletService
  435. .getCurrentServletRequest();
  436. if (currentRequest != null) {
  437. HttpSession httpSession = currentRequest.getSession(false);
  438. if (httpSession != null) {
  439. Map<Class<?>, CurrentInstance> oldCurrent = CurrentInstance
  440. .setCurrent((VaadinSession) null);
  441. try {
  442. VaadinServletService service = (VaadinServletService) VaadinService
  443. .getCurrent();
  444. session = service.findVaadinSession(
  445. new VaadinServletRequest(currentRequest,
  446. service));
  447. } finally {
  448. /*
  449. * Clear some state set by findVaadinSession to
  450. * avoid accidentally depending on it when coding on
  451. * e.g. static request handling.
  452. */
  453. CurrentInstance.restoreInstances(oldCurrent);
  454. currentRequest.removeAttribute(
  455. VaadinSession.class.getName());
  456. }
  457. }
  458. }
  459. }
  460. if (session != null) {
  461. String name = ApplicationRunnerServlet.class.getName()
  462. + ".deploymentConfiguration";
  463. try {
  464. session.lock();
  465. configuration = (DeploymentConfiguration) session
  466. .getAttribute(name);
  467. if (configuration == null) {
  468. ApplicationRunnerServlet servlet = (ApplicationRunnerServlet) VaadinServlet
  469. .getCurrent();
  470. Class<?> classToRun;
  471. try {
  472. classToRun = servlet.getClassToRun();
  473. } catch (ClassNotFoundException e) {
  474. /*
  475. * This happens e.g. if the UI class defined in the
  476. * URL is not found or if this servlet just serves
  477. * static resources while there's some other servlet
  478. * that serves the UI (e.g. when using /run-push/).
  479. */
  480. return originalConfiguration;
  481. }
  482. CustomDeploymentConfiguration customDeploymentConfiguration = classToRun
  483. .getAnnotation(
  484. CustomDeploymentConfiguration.class);
  485. if (customDeploymentConfiguration != null) {
  486. Properties initParameters = new Properties(
  487. originalConfiguration.getInitParameters());
  488. for (Conf entry : customDeploymentConfiguration
  489. .value()) {
  490. initParameters.put(entry.name(), entry.value());
  491. }
  492. configuration = new DefaultDeploymentConfiguration(
  493. servlet.getClass(), initParameters);
  494. } else {
  495. configuration = originalConfiguration;
  496. }
  497. session.setAttribute(name, configuration);
  498. }
  499. } finally {
  500. session.unlock();
  501. }
  502. CurrentInstance.set(DeploymentConfiguration.class,
  503. configuration);
  504. } else {
  505. configuration = originalConfiguration;
  506. }
  507. }
  508. return configuration;
  509. }
  510. }