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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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.SessionInitEvent;
  47. import com.vaadin.server.SessionInitListener;
  48. import com.vaadin.server.SystemMessages;
  49. import com.vaadin.server.SystemMessagesInfo;
  50. import com.vaadin.server.SystemMessagesProvider;
  51. import com.vaadin.server.UIClassSelectionEvent;
  52. import com.vaadin.server.UIProvider;
  53. import com.vaadin.server.VaadinRequest;
  54. import com.vaadin.server.VaadinService;
  55. import com.vaadin.server.VaadinServlet;
  56. import com.vaadin.server.VaadinServletRequest;
  57. import com.vaadin.server.VaadinServletService;
  58. import com.vaadin.server.VaadinSession;
  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(new SessionInitListener() {
  120. @Override
  121. public void sessionInit(SessionInitEvent event)
  122. throws ServiceException {
  123. onVaadinSessionStarted(event.getRequest(), event.getSession());
  124. }
  125. });
  126. }
  127. private void addDirectories(File parent, LinkedHashSet<String> packages,
  128. String parentPackage) {
  129. packages.add(parentPackage);
  130. for (File f : parent.listFiles()) {
  131. if (f.isDirectory()) {
  132. String newPackage = parentPackage + "." + f.getName();
  133. addDirectories(f, packages, newPackage);
  134. }
  135. }
  136. }
  137. @Override
  138. protected void service(HttpServletRequest request,
  139. HttpServletResponse response) throws ServletException, IOException {
  140. this.request.set(request);
  141. try {
  142. super.service(request, response);
  143. } catch (ServletException e) {
  144. String redirectTarget = ApplicationRunnerRedirectException
  145. .extractRedirectTarget(e);
  146. if (redirectTarget != null) {
  147. response.sendRedirect(redirectTarget + "?restartApplication");
  148. } else {
  149. // Not the exception we were looking for, just rethrow
  150. throw e;
  151. }
  152. } finally {
  153. this.request.set(null);
  154. }
  155. }
  156. @Override
  157. protected URL getApplicationUrl(HttpServletRequest request)
  158. throws MalformedURLException {
  159. URL url = super.getApplicationUrl(request);
  160. String path = url.toString();
  161. path += getApplicationRunnerApplicationClassName(request);
  162. path += "/";
  163. return new URL(path);
  164. }
  165. @Override
  166. protected Class<? extends LegacyApplication> getApplicationClass()
  167. throws ClassNotFoundException {
  168. return getClassToRun().asSubclass(LegacyApplication.class);
  169. }
  170. @Override
  171. protected boolean shouldCreateApplication(HttpServletRequest request)
  172. throws ServletException {
  173. try {
  174. return LegacyApplication.class.isAssignableFrom(getClassToRun());
  175. } catch (ClassNotFoundException e) {
  176. throw new ServletException(e);
  177. }
  178. }
  179. protected void onVaadinSessionStarted(VaadinRequest request,
  180. VaadinSession session) throws ServiceException {
  181. try {
  182. final Class<?> classToRun = getClassToRun();
  183. if (UI.class.isAssignableFrom(classToRun)) {
  184. session.addUIProvider(
  185. new ApplicationRunnerUIProvider(classToRun));
  186. } else if (LegacyApplication.class.isAssignableFrom(classToRun)) {
  187. // Avoid using own UIProvider for legacy Application
  188. } else if (UIProvider.class.isAssignableFrom(classToRun)) {
  189. session.addUIProvider((UIProvider) classToRun.newInstance());
  190. } else {
  191. throw new ServiceException(classToRun.getCanonicalName()
  192. + " is neither an Application nor a UI");
  193. }
  194. } catch (final IllegalAccessException e) {
  195. throw new ServiceException(e);
  196. } catch (final InstantiationException e) {
  197. throw new ServiceException(e);
  198. } catch (final ClassNotFoundException e) {
  199. throw new ServiceException(new InstantiationException(
  200. "Failed to load application class: "
  201. + getApplicationRunnerApplicationClassName(
  202. (VaadinServletRequest) request)));
  203. }
  204. }
  205. private String getApplicationRunnerApplicationClassName(
  206. HttpServletRequest request) {
  207. return getApplicationRunnerURIs(request).applicationClassname;
  208. }
  209. private static final class ProxyDeploymentConfiguration
  210. implements InvocationHandler, Serializable {
  211. private final DeploymentConfiguration originalConfiguration;
  212. private ProxyDeploymentConfiguration(
  213. DeploymentConfiguration originalConfiguration) {
  214. this.originalConfiguration = originalConfiguration;
  215. }
  216. @Override
  217. public Object invoke(Object proxy, Method method, Object[] args)
  218. throws Throwable {
  219. if (method.getDeclaringClass() == DeploymentConfiguration.class) {
  220. // Find the configuration instance to delegate to
  221. DeploymentConfiguration configuration = findDeploymentConfiguration(
  222. originalConfiguration);
  223. return method.invoke(configuration, args);
  224. } else {
  225. return method.invoke(proxy, args);
  226. }
  227. }
  228. }
  229. private static final class ApplicationRunnerUIProvider extends UIProvider {
  230. private final Class<?> classToRun;
  231. private ApplicationRunnerUIProvider(Class<?> classToRun) {
  232. this.classToRun = classToRun;
  233. }
  234. @Override
  235. public Class<? extends UI> getUIClass(UIClassSelectionEvent event) {
  236. return (Class<? extends UI>) classToRun;
  237. }
  238. }
  239. // TODO Don't need to use a data object now that there's only one field
  240. private static class URIS {
  241. // String staticFilesPath;
  242. // String applicationURI;
  243. // String context;
  244. // String runner;
  245. String applicationClassname;
  246. }
  247. /**
  248. * Parses application runner URIs.
  249. *
  250. * If request URL is e.g.
  251. * http://localhost:8080/vaadin/run/com.vaadin.demo.Calc then
  252. * <ul>
  253. * <li>context=vaadin</li>
  254. * <li>Runner servlet=run</li>
  255. * <li>Vaadin application=com.vaadin.demo.Calc</li>
  256. * </ul>
  257. *
  258. * @param request
  259. * @return string array containing widgetset URI, application URI and
  260. * context, runner, application classname
  261. */
  262. private static URIS getApplicationRunnerURIs(HttpServletRequest request) {
  263. final String[] urlParts = request.getRequestURI().toString()
  264. .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. }