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.

BootstrapHandler.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. /*
  2. * Copyright 2000-2014 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.server;
  17. import java.io.BufferedWriter;
  18. import java.io.IOException;
  19. import java.io.OutputStreamWriter;
  20. import java.io.Serializable;
  21. import java.util.ArrayList;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Locale;
  25. import java.util.Map;
  26. import java.util.Map.Entry;
  27. import java.util.Set;
  28. import javax.servlet.http.HttpServletResponse;
  29. import org.jsoup.nodes.DataNode;
  30. import org.jsoup.nodes.Document;
  31. import org.jsoup.nodes.DocumentType;
  32. import org.jsoup.nodes.Element;
  33. import org.jsoup.nodes.Node;
  34. import org.jsoup.parser.Tag;
  35. import com.vaadin.shared.ApplicationConstants;
  36. import com.vaadin.shared.Version;
  37. import com.vaadin.shared.communication.PushMode;
  38. import com.vaadin.ui.UI;
  39. import elemental.json.Json;
  40. import elemental.json.JsonException;
  41. import elemental.json.JsonObject;
  42. import elemental.json.impl.JsonUtil;
  43. /**
  44. *
  45. * @author Vaadin Ltd
  46. * @since 7.0.0
  47. *
  48. * @deprecated As of 7.0. Will likely change or be removed in a future version
  49. */
  50. @Deprecated
  51. public abstract class BootstrapHandler extends SynchronizedRequestHandler {
  52. /**
  53. * Parameter that is added to the UI init request if the session has already
  54. * been restarted when generating the bootstrap HTML and ?restartApplication
  55. * should thus be ignored when handling the UI init request.
  56. */
  57. public static final String IGNORE_RESTART_PARAM = "ignoreRestart";
  58. protected class BootstrapContext implements Serializable {
  59. private final VaadinResponse response;
  60. private final BootstrapFragmentResponse bootstrapResponse;
  61. private String widgetsetName;
  62. private String themeName;
  63. private String appId;
  64. private PushMode pushMode;
  65. public BootstrapContext(VaadinResponse response,
  66. BootstrapFragmentResponse bootstrapResponse) {
  67. this.response = response;
  68. this.bootstrapResponse = bootstrapResponse;
  69. }
  70. public VaadinResponse getResponse() {
  71. return response;
  72. }
  73. public VaadinRequest getRequest() {
  74. return bootstrapResponse.getRequest();
  75. }
  76. public VaadinSession getSession() {
  77. return bootstrapResponse.getSession();
  78. }
  79. public Class<? extends UI> getUIClass() {
  80. return bootstrapResponse.getUiClass();
  81. }
  82. public String getWidgetsetName() {
  83. if (widgetsetName == null) {
  84. widgetsetName = getWidgetsetForUI(this);
  85. }
  86. return widgetsetName;
  87. }
  88. public String getThemeName() {
  89. if (themeName == null) {
  90. themeName = findAndEscapeThemeName(this);
  91. }
  92. return themeName;
  93. }
  94. public PushMode getPushMode() {
  95. if (pushMode == null) {
  96. UICreateEvent event = new UICreateEvent(getRequest(),
  97. getUIClass());
  98. pushMode = getBootstrapResponse().getUIProvider().getPushMode(
  99. event);
  100. if (pushMode == null) {
  101. pushMode = getRequest().getService()
  102. .getDeploymentConfiguration().getPushMode();
  103. }
  104. if (pushMode.isEnabled()
  105. && !getRequest().getService().ensurePushAvailable()) {
  106. /*
  107. * Fall back if not supported (ensurePushAvailable will log
  108. * information to the developer the first time this happens)
  109. */
  110. pushMode = PushMode.DISABLED;
  111. }
  112. }
  113. return pushMode;
  114. }
  115. public String getAppId() {
  116. if (appId == null) {
  117. appId = getRequest().getService().getMainDivId(getSession(),
  118. getRequest(), getUIClass());
  119. }
  120. return appId;
  121. }
  122. public BootstrapFragmentResponse getBootstrapResponse() {
  123. return bootstrapResponse;
  124. }
  125. }
  126. @Override
  127. protected boolean canHandleRequest(VaadinRequest request) {
  128. // We do not want to handle /APP requests here, instead let it fall
  129. // through and produce a 404
  130. return !ServletPortletHelper.isAppRequest(request);
  131. }
  132. @Override
  133. public boolean synchronizedHandleRequest(VaadinSession session,
  134. VaadinRequest request, VaadinResponse response) throws IOException {
  135. try {
  136. List<UIProvider> uiProviders = session.getUIProviders();
  137. UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent(
  138. request);
  139. // Find UI provider and UI class
  140. Class<? extends UI> uiClass = null;
  141. UIProvider provider = null;
  142. for (UIProvider p : uiProviders) {
  143. uiClass = p.getUIClass(classSelectionEvent);
  144. // If we found something
  145. if (uiClass != null) {
  146. provider = p;
  147. break;
  148. }
  149. }
  150. if (provider == null) {
  151. // Can't generate bootstrap if no UI provider matches
  152. return false;
  153. }
  154. BootstrapContext context = new BootstrapContext(response,
  155. new BootstrapFragmentResponse(this, request, session,
  156. uiClass, new ArrayList<Node>(), provider));
  157. setupMainDiv(context);
  158. BootstrapFragmentResponse fragmentResponse = context
  159. .getBootstrapResponse();
  160. session.modifyBootstrapResponse(fragmentResponse);
  161. String html = getBootstrapHtml(context);
  162. writeBootstrapPage(response, html);
  163. } catch (JsonException e) {
  164. writeError(response, e);
  165. }
  166. return true;
  167. }
  168. private String getBootstrapHtml(BootstrapContext context) {
  169. VaadinRequest request = context.getRequest();
  170. VaadinResponse response = context.getResponse();
  171. VaadinService vaadinService = request.getService();
  172. BootstrapFragmentResponse fragmentResponse = context
  173. .getBootstrapResponse();
  174. if (vaadinService.isStandalone(request)) {
  175. Map<String, Object> headers = new LinkedHashMap<String, Object>();
  176. Document document = Document.createShell("");
  177. BootstrapPageResponse pageResponse = new BootstrapPageResponse(
  178. this, request, context.getSession(), context.getUIClass(),
  179. document, headers, fragmentResponse.getUIProvider());
  180. List<Node> fragmentNodes = fragmentResponse.getFragmentNodes();
  181. Element body = document.body();
  182. for (Node node : fragmentNodes) {
  183. body.appendChild(node);
  184. }
  185. setupStandaloneDocument(context, pageResponse);
  186. context.getSession().modifyBootstrapResponse(pageResponse);
  187. sendBootstrapHeaders(response, headers);
  188. return document.outerHtml();
  189. } else {
  190. StringBuilder sb = new StringBuilder();
  191. for (Node node : fragmentResponse.getFragmentNodes()) {
  192. if (sb.length() != 0) {
  193. sb.append('\n');
  194. }
  195. sb.append(node.outerHtml());
  196. }
  197. return sb.toString();
  198. }
  199. }
  200. private void sendBootstrapHeaders(VaadinResponse response,
  201. Map<String, Object> headers) {
  202. Set<Entry<String, Object>> entrySet = headers.entrySet();
  203. for (Entry<String, Object> header : entrySet) {
  204. Object value = header.getValue();
  205. if (value instanceof String) {
  206. response.setHeader(header.getKey(), (String) value);
  207. } else if (value instanceof Long) {
  208. response.setDateHeader(header.getKey(),
  209. ((Long) value).longValue());
  210. } else {
  211. throw new RuntimeException("Unsupported header value: " + value);
  212. }
  213. }
  214. }
  215. private void writeBootstrapPage(VaadinResponse response, String html)
  216. throws IOException {
  217. response.setContentType("text/html");
  218. BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
  219. response.getOutputStream(), "UTF-8"));
  220. writer.append(html);
  221. writer.close();
  222. }
  223. private void setupStandaloneDocument(BootstrapContext context,
  224. BootstrapPageResponse response) {
  225. response.setHeader("Cache-Control", "no-cache");
  226. response.setHeader("Pragma", "no-cache");
  227. response.setDateHeader("Expires", 0);
  228. Document document = response.getDocument();
  229. DocumentType doctype = new DocumentType("html", "", "",
  230. document.baseUri());
  231. document.child(0).before(doctype);
  232. Element head = document.head();
  233. head.appendElement("meta").attr("http-equiv", "Content-Type")
  234. .attr("content", "text/html; charset=utf-8");
  235. /*
  236. * Enable Chrome Frame in all versions of IE if installed.
  237. */
  238. head.appendElement("meta").attr("http-equiv", "X-UA-Compatible")
  239. .attr("content", "IE=11;chrome=1");
  240. String title = response.getUIProvider().getPageTitle(
  241. new UICreateEvent(context.getRequest(), context.getUIClass()));
  242. if (title != null) {
  243. head.appendElement("title").appendText(title);
  244. }
  245. head.appendElement("style").attr("type", "text/css")
  246. .appendText("html, body {height:100%;margin:0;}");
  247. // Add favicon links
  248. String themeName = context.getThemeName();
  249. if (themeName != null) {
  250. String themeUri = getThemeUri(context, themeName);
  251. head.appendElement("link").attr("rel", "shortcut icon")
  252. .attr("type", "image/vnd.microsoft.icon")
  253. .attr("href", themeUri + "/favicon.ico");
  254. head.appendElement("link").attr("rel", "icon")
  255. .attr("type", "image/vnd.microsoft.icon")
  256. .attr("href", themeUri + "/favicon.ico");
  257. }
  258. Element body = document.body();
  259. body.attr("scroll", "auto");
  260. body.addClass(ApplicationConstants.GENERATED_BODY_CLASSNAME);
  261. }
  262. protected String getMainDivStyle(BootstrapContext context) {
  263. return null;
  264. }
  265. public String getWidgetsetForUI(BootstrapContext context) {
  266. VaadinRequest request = context.getRequest();
  267. UICreateEvent event = new UICreateEvent(context.getRequest(),
  268. context.getUIClass());
  269. String widgetset = context.getBootstrapResponse().getUIProvider()
  270. .getWidgetset(event);
  271. if (widgetset == null) {
  272. widgetset = request.getService().getConfiguredWidgetset(request);
  273. }
  274. widgetset = VaadinServlet.stripSpecialChars(widgetset);
  275. return widgetset;
  276. }
  277. /**
  278. * Method to write the div element into which that actual Vaadin application
  279. * is rendered.
  280. * <p>
  281. * Override this method if you want to add some custom html around around
  282. * the div element into which the actual Vaadin application will be
  283. * rendered.
  284. *
  285. * @param context
  286. *
  287. * @throws IOException
  288. */
  289. private void setupMainDiv(BootstrapContext context) throws IOException {
  290. String style = getMainDivStyle(context);
  291. /*- Add classnames;
  292. * .v-app
  293. * .v-app-loading
  294. * .v-app-<simpleName for app class>
  295. *- Additionally added from javascript:
  296. * <themeName, remove non-alphanum>
  297. */
  298. List<Node> fragmentNodes = context.getBootstrapResponse()
  299. .getFragmentNodes();
  300. Element mainDiv = new Element(Tag.valueOf("div"), "");
  301. mainDiv.attr("id", context.getAppId());
  302. mainDiv.addClass("v-app");
  303. mainDiv.addClass(context.getThemeName());
  304. if (style != null && style.length() != 0) {
  305. mainDiv.attr("style", style);
  306. }
  307. mainDiv.appendElement("div").addClass("v-app-loading");
  308. mainDiv.appendElement("noscript")
  309. .append("You have to enable javascript in your browser to use an application built with Vaadin.");
  310. fragmentNodes.add(mainDiv);
  311. VaadinRequest request = context.getRequest();
  312. VaadinService vaadinService = request.getService();
  313. String vaadinLocation = vaadinService.getStaticFileLocation(request)
  314. + "/VAADIN/";
  315. if (context.getPushMode().isEnabled()) {
  316. // Load client-side dependencies for push support
  317. String pushJS = vaadinLocation;
  318. if (context.getRequest().getService().getDeploymentConfiguration()
  319. .isProductionMode()) {
  320. pushJS += ApplicationConstants.VAADIN_PUSH_JS;
  321. } else {
  322. pushJS += ApplicationConstants.VAADIN_PUSH_DEBUG_JS;
  323. }
  324. fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr(
  325. "type", "text/javascript").attr("src", pushJS));
  326. }
  327. String bootstrapLocation = vaadinLocation
  328. + ApplicationConstants.VAADIN_BOOTSTRAP_JS;
  329. fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type",
  330. "text/javascript").attr("src", bootstrapLocation));
  331. Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr(
  332. "type", "text/javascript");
  333. StringBuilder builder = new StringBuilder();
  334. builder.append("//<![CDATA[\n");
  335. builder.append("if (!window.vaadin) alert("
  336. + JsonUtil.quote("Failed to load the bootstrap javascript: "
  337. + bootstrapLocation) + ");\n");
  338. appendMainScriptTagContents(context, builder);
  339. builder.append("//]]>");
  340. mainScriptTag.appendChild(new DataNode(builder.toString(),
  341. mainScriptTag.baseUri()));
  342. fragmentNodes.add(mainScriptTag);
  343. }
  344. protected void appendMainScriptTagContents(BootstrapContext context,
  345. StringBuilder builder) throws IOException {
  346. JsonObject appConfig = getApplicationParameters(context);
  347. boolean isDebug = !context.getSession().getConfiguration()
  348. .isProductionMode();
  349. if (isDebug) {
  350. /*
  351. * Add tracking needed for getting bootstrap metrics to the client
  352. * side Profiler if another implementation hasn't already been
  353. * added.
  354. */
  355. builder.append("if (typeof window.__gwtStatsEvent != 'function') {\n");
  356. builder.append("vaadin.gwtStatsEvents = [];\n");
  357. builder.append("window.__gwtStatsEvent = function(event) {vaadin.gwtStatsEvents.push(event); return true;};\n");
  358. builder.append("}\n");
  359. }
  360. builder.append("vaadin.initApplication(\"");
  361. builder.append(context.getAppId());
  362. builder.append("\",");
  363. appendJsonObject(builder, appConfig, isDebug);
  364. builder.append(");\n");
  365. }
  366. private static void appendJsonObject(StringBuilder builder,
  367. JsonObject jsonObject, boolean isDebug) {
  368. if (isDebug) {
  369. builder.append(JsonUtil.stringify(jsonObject, 4));
  370. } else {
  371. builder.append(JsonUtil.stringify(jsonObject));
  372. }
  373. }
  374. protected JsonObject getApplicationParameters(BootstrapContext context)
  375. throws PaintException {
  376. VaadinRequest request = context.getRequest();
  377. VaadinSession session = context.getSession();
  378. VaadinService vaadinService = request.getService();
  379. JsonObject appConfig = Json.createObject();
  380. String themeName = context.getThemeName();
  381. if (themeName != null) {
  382. appConfig.put("theme", themeName);
  383. }
  384. // Ignore restartApplication that might be passed to UI init
  385. if (request
  386. .getParameter(VaadinService.URL_PARAMETER_RESTART_APPLICATION) != null) {
  387. appConfig.put("extraParams", "&" + IGNORE_RESTART_PARAM + "=1");
  388. }
  389. JsonObject versionInfo = Json.createObject();
  390. versionInfo.put("vaadinVersion", Version.getFullVersion());
  391. appConfig.put("versionInfo", versionInfo);
  392. appConfig.put("widgetset", context.getWidgetsetName());
  393. // Use locale from session if set, else from the request
  394. Locale locale = ServletPortletHelper.findLocale(null,
  395. context.getSession(), context.getRequest());
  396. // Get system messages
  397. SystemMessages systemMessages = vaadinService.getSystemMessages(locale,
  398. request);
  399. if (systemMessages != null) {
  400. // Write the CommunicationError -message to client
  401. JsonObject comErrMsg = Json.createObject();
  402. comErrMsg.put("caption",
  403. systemMessages.getCommunicationErrorCaption());
  404. comErrMsg.put("message",
  405. systemMessages.getCommunicationErrorMessage());
  406. if (systemMessages.getCommunicationErrorURL() == null) {
  407. comErrMsg.put("url", Json.createNull());
  408. } else {
  409. comErrMsg.put("url", systemMessages.getCommunicationErrorURL());
  410. }
  411. appConfig.put("comErrMsg", comErrMsg);
  412. JsonObject authErrMsg = Json.createObject();
  413. authErrMsg.put("caption",
  414. systemMessages.getAuthenticationErrorCaption());
  415. authErrMsg.put("message",
  416. systemMessages.getAuthenticationErrorMessage());
  417. if (systemMessages.getAuthenticationErrorURL() == null) {
  418. authErrMsg.put("url", Json.createNull());
  419. } else {
  420. authErrMsg.put("url", systemMessages.getAuthenticationErrorURL());
  421. }
  422. appConfig.put("authErrMsg", authErrMsg);
  423. JsonObject sessExpMsg = Json.createObject();
  424. sessExpMsg
  425. .put("caption", systemMessages.getSessionExpiredCaption());
  426. sessExpMsg
  427. .put("message", systemMessages.getSessionExpiredMessage());
  428. if (systemMessages.getSessionExpiredURL() == null) {
  429. sessExpMsg.put("url", Json.createNull());
  430. } else {
  431. sessExpMsg.put("url", systemMessages.getSessionExpiredURL());
  432. }
  433. appConfig.put("sessExpMsg", sessExpMsg);
  434. }
  435. // getStaticFileLocation documented to never end with a slash
  436. // vaadinDir should always end with a slash
  437. String vaadinDir = vaadinService.getStaticFileLocation(request)
  438. + "/VAADIN/";
  439. appConfig.put(ApplicationConstants.VAADIN_DIR_URL, vaadinDir);
  440. if (!session.getConfiguration().isProductionMode()) {
  441. appConfig.put("debug", true);
  442. }
  443. if (vaadinService.isStandalone(request)) {
  444. appConfig.put("standalone", true);
  445. }
  446. appConfig.put("heartbeatInterval", vaadinService
  447. .getDeploymentConfiguration().getHeartbeatInterval());
  448. String serviceUrl = getServiceUrl(context);
  449. if (serviceUrl != null) {
  450. appConfig.put(ApplicationConstants.SERVICE_URL, serviceUrl);
  451. }
  452. return appConfig;
  453. }
  454. protected abstract String getServiceUrl(BootstrapContext context);
  455. /**
  456. * Get the URI for the application theme.
  457. *
  458. * A portal-wide default theme is fetched from the portal shared resource
  459. * directory (if any), other themes from the portlet.
  460. *
  461. * @param context
  462. * @param themeName
  463. *
  464. * @return
  465. */
  466. public String getThemeUri(BootstrapContext context, String themeName) {
  467. VaadinRequest request = context.getRequest();
  468. final String staticFilePath = request.getService()
  469. .getStaticFileLocation(request);
  470. return staticFilePath + "/" + VaadinServlet.THEME_DIR_PATH + '/'
  471. + themeName;
  472. }
  473. /**
  474. * Override if required
  475. *
  476. * @param context
  477. * @return
  478. */
  479. public String getThemeName(BootstrapContext context) {
  480. UICreateEvent event = new UICreateEvent(context.getRequest(),
  481. context.getUIClass());
  482. return context.getBootstrapResponse().getUIProvider().getTheme(event);
  483. }
  484. /**
  485. * Don not override.
  486. *
  487. * @param context
  488. * @return
  489. */
  490. public String findAndEscapeThemeName(BootstrapContext context) {
  491. String themeName = getThemeName(context);
  492. if (themeName == null) {
  493. VaadinRequest request = context.getRequest();
  494. themeName = request.getService().getConfiguredTheme(request);
  495. }
  496. // XSS preventation, theme names shouldn't contain special chars anyway.
  497. // The servlet denies them via url parameter.
  498. themeName = VaadinServlet.stripSpecialChars(themeName);
  499. return themeName;
  500. }
  501. protected void writeError(VaadinResponse response, Throwable e)
  502. throws IOException {
  503. response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
  504. e.getLocalizedMessage());
  505. }
  506. }