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 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /*
  2. * Copyright 2011 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.Map;
  25. import java.util.Map.Entry;
  26. import java.util.Set;
  27. import javax.servlet.http.HttpServletResponse;
  28. import org.jsoup.nodes.DataNode;
  29. import org.jsoup.nodes.Document;
  30. import org.jsoup.nodes.DocumentType;
  31. import org.jsoup.nodes.Element;
  32. import org.jsoup.nodes.Node;
  33. import org.jsoup.parser.Tag;
  34. import com.vaadin.Application;
  35. import com.vaadin.UIRequiresMoreInformationException;
  36. import com.vaadin.external.json.JSONException;
  37. import com.vaadin.external.json.JSONObject;
  38. import com.vaadin.shared.ApplicationConstants;
  39. import com.vaadin.shared.Version;
  40. import com.vaadin.shared.ui.ui.UIConstants;
  41. import com.vaadin.ui.UI;
  42. public abstract class BootstrapHandler implements RequestHandler {
  43. protected class BootstrapContext implements Serializable {
  44. private final WrappedResponse response;
  45. private final BootstrapFragmentResponse bootstrapResponse;
  46. private String widgetsetName;
  47. private String themeName;
  48. private String appId;
  49. public BootstrapContext(WrappedResponse response,
  50. BootstrapFragmentResponse bootstrapResponse) {
  51. this.response = response;
  52. this.bootstrapResponse = bootstrapResponse;
  53. }
  54. public WrappedResponse getResponse() {
  55. return response;
  56. }
  57. public WrappedRequest getRequest() {
  58. return bootstrapResponse.getRequest();
  59. }
  60. public Application getApplication() {
  61. return bootstrapResponse.getApplication();
  62. }
  63. public Integer getUIId() {
  64. return bootstrapResponse.getUIId();
  65. }
  66. public UI getUI() {
  67. return bootstrapResponse.getUI();
  68. }
  69. public String getWidgetsetName() {
  70. if (widgetsetName == null) {
  71. UI uI = getUI();
  72. if (uI != null) {
  73. widgetsetName = getWidgetsetForUI(this);
  74. }
  75. }
  76. return widgetsetName;
  77. }
  78. public String getThemeName() {
  79. if (themeName == null) {
  80. UI uI = getUI();
  81. if (uI != null) {
  82. themeName = findAndEscapeThemeName(this);
  83. }
  84. }
  85. return themeName;
  86. }
  87. public String getAppId() {
  88. if (appId == null) {
  89. appId = getApplicationId(this);
  90. }
  91. return appId;
  92. }
  93. public BootstrapFragmentResponse getBootstrapResponse() {
  94. return bootstrapResponse;
  95. }
  96. }
  97. @Override
  98. public boolean handleRequest(Application application,
  99. WrappedRequest request, WrappedResponse response)
  100. throws IOException {
  101. // TODO Should all urls be handled here?
  102. Integer uiId = null;
  103. try {
  104. UI uI = application.getUIForRequest(request);
  105. if (uI == null) {
  106. writeError(response, new Throwable("No UI found"));
  107. return true;
  108. }
  109. uiId = Integer.valueOf(uI.getUIId());
  110. } catch (UIRequiresMoreInformationException e) {
  111. // Just keep going without uiId
  112. }
  113. try {
  114. BootstrapContext context = createContext(request, response,
  115. application, uiId);
  116. setupMainDiv(context);
  117. BootstrapFragmentResponse fragmentResponse = context
  118. .getBootstrapResponse();
  119. application.modifyBootstrapResponse(fragmentResponse);
  120. String html = getBootstrapHtml(context);
  121. writeBootstrapPage(response, html);
  122. } catch (JSONException e) {
  123. writeError(response, e);
  124. }
  125. return true;
  126. }
  127. private String getBootstrapHtml(BootstrapContext context) {
  128. WrappedRequest request = context.getRequest();
  129. WrappedResponse response = context.getResponse();
  130. DeploymentConfiguration deploymentConfiguration = request
  131. .getDeploymentConfiguration();
  132. BootstrapFragmentResponse fragmentResponse = context
  133. .getBootstrapResponse();
  134. if (deploymentConfiguration.isStandalone(request)) {
  135. Map<String, Object> headers = new LinkedHashMap<String, Object>();
  136. Document document = Document.createShell("");
  137. BootstrapPageResponse pageResponse = new BootstrapPageResponse(
  138. this, request, context.getApplication(), context.getUIId(),
  139. document, headers);
  140. List<Node> fragmentNodes = fragmentResponse.getFragmentNodes();
  141. Element body = document.body();
  142. for (Node node : fragmentNodes) {
  143. body.appendChild(node);
  144. }
  145. setupStandaloneDocument(context, pageResponse);
  146. context.getApplication().modifyBootstrapResponse(pageResponse);
  147. sendBootstrapHeaders(response, headers);
  148. return document.outerHtml();
  149. } else {
  150. StringBuilder sb = new StringBuilder();
  151. for (Node node : fragmentResponse.getFragmentNodes()) {
  152. if (sb.length() != 0) {
  153. sb.append('\n');
  154. }
  155. sb.append(node.outerHtml());
  156. }
  157. return sb.toString();
  158. }
  159. }
  160. private void sendBootstrapHeaders(WrappedResponse response,
  161. Map<String, Object> headers) {
  162. Set<Entry<String, Object>> entrySet = headers.entrySet();
  163. for (Entry<String, Object> header : entrySet) {
  164. Object value = header.getValue();
  165. if (value instanceof String) {
  166. response.setHeader(header.getKey(), (String) value);
  167. } else if (value instanceof Long) {
  168. response.setDateHeader(header.getKey(),
  169. ((Long) value).longValue());
  170. } else {
  171. throw new RuntimeException("Unsupported header value: " + value);
  172. }
  173. }
  174. }
  175. private void writeBootstrapPage(WrappedResponse response, String html)
  176. throws IOException {
  177. response.setContentType("text/html");
  178. BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
  179. response.getOutputStream(), "UTF-8"));
  180. writer.append(html);
  181. writer.close();
  182. }
  183. private void setupStandaloneDocument(BootstrapContext context,
  184. BootstrapPageResponse response) {
  185. response.setHeader("Cache-Control", "no-cache");
  186. response.setHeader("Pragma", "no-cache");
  187. response.setDateHeader("Expires", 0);
  188. Document document = response.getDocument();
  189. DocumentType doctype = new DocumentType("html",
  190. "-//W3C//DTD XHTML 1.0 Transitional//EN",
  191. "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd",
  192. document.baseUri());
  193. document.child(0).before(doctype);
  194. document.body().parent().attr("xmlns", "http://www.w3.org/1999/xhtml");
  195. Element head = document.head();
  196. head.appendElement("meta").attr("http-equiv", "Content-Type")
  197. .attr("content", "text/html; charset=utf-8");
  198. // Chrome frame in all versions of IE (only if Chrome frame is
  199. // installed)
  200. head.appendElement("meta").attr("http-equiv", "X-UA-Compatible")
  201. .attr("content", "chrome=1");
  202. UI uI = context.getUI();
  203. String title = ((uI == null || uI.getCaption() == null) ? "" : uI
  204. .getCaption());
  205. head.appendElement("title").appendText(title);
  206. head.appendElement("style").attr("type", "text/css")
  207. .appendText("html, body {height:100%;margin:0;}");
  208. // Add favicon links
  209. String themeName = context.getThemeName();
  210. if (themeName != null) {
  211. String themeUri = getThemeUri(context, themeName);
  212. head.appendElement("link").attr("rel", "shortcut icon")
  213. .attr("type", "image/vnd.microsoft.icon")
  214. .attr("href", themeUri + "/favicon.ico");
  215. head.appendElement("link").attr("rel", "icon")
  216. .attr("type", "image/vnd.microsoft.icon")
  217. .attr("href", themeUri + "/favicon.ico");
  218. }
  219. Element body = document.body();
  220. body.attr("scroll", "auto");
  221. body.addClass(ApplicationConstants.GENERATED_BODY_CLASSNAME);
  222. }
  223. public BootstrapContext createContext(WrappedRequest request,
  224. WrappedResponse response, Application application, Integer uiId) {
  225. BootstrapContext context = new BootstrapContext(response,
  226. new BootstrapFragmentResponse(this, request, application, uiId,
  227. new ArrayList<Node>()));
  228. return context;
  229. }
  230. protected String getMainDivStyle(BootstrapContext context) {
  231. return null;
  232. }
  233. /**
  234. * Creates and returns a unique ID for the DIV where the application is to
  235. * be rendered.
  236. *
  237. * @param context
  238. *
  239. * @return the id to use in the DOM
  240. */
  241. protected abstract String getApplicationId(BootstrapContext context);
  242. public String getWidgetsetForUI(BootstrapContext context) {
  243. UI uI = context.getUI();
  244. WrappedRequest request = context.getRequest();
  245. String widgetset = uI.getApplication().getWidgetsetForUI(uI);
  246. if (widgetset == null) {
  247. widgetset = request.getDeploymentConfiguration()
  248. .getConfiguredWidgetset(request);
  249. }
  250. widgetset = AbstractApplicationServlet.stripSpecialChars(widgetset);
  251. return widgetset;
  252. }
  253. /**
  254. * Method to write the div element into which that actual Vaadin application
  255. * is rendered.
  256. * <p>
  257. * Override this method if you want to add some custom html around around
  258. * the div element into which the actual Vaadin application will be
  259. * rendered.
  260. *
  261. * @param context
  262. *
  263. * @throws IOException
  264. * @throws JSONException
  265. */
  266. private void setupMainDiv(BootstrapContext context) throws IOException,
  267. JSONException {
  268. String style = getMainDivStyle(context);
  269. /*- Add classnames;
  270. * .v-app
  271. * .v-app-loading
  272. * .v-app-<simpleName for app class>
  273. *- Additionally added from javascript:
  274. * .v-theme-<themeName, remove non-alphanum>
  275. */
  276. String appClass = "v-app-"
  277. + context.getApplication().getClass().getSimpleName();
  278. String classNames = "v-app " + appClass;
  279. List<Node> fragmentNodes = context.getBootstrapResponse()
  280. .getFragmentNodes();
  281. Element mainDiv = new Element(Tag.valueOf("div"), "");
  282. mainDiv.attr("id", context.getAppId());
  283. mainDiv.addClass(classNames);
  284. if (style != null && style.length() != 0) {
  285. mainDiv.attr("style", style);
  286. }
  287. mainDiv.appendElement("div").addClass("v-app-loading");
  288. mainDiv.appendElement("noscript")
  289. .append("You have to enable javascript in your browser to use an application built with Vaadin.");
  290. fragmentNodes.add(mainDiv);
  291. WrappedRequest request = context.getRequest();
  292. DeploymentConfiguration deploymentConfiguration = request
  293. .getDeploymentConfiguration();
  294. String staticFileLocation = deploymentConfiguration
  295. .getStaticFileLocation(request);
  296. fragmentNodes
  297. .add(new Element(Tag.valueOf("iframe"), "")
  298. .attr("tabIndex", "-1")
  299. .attr("id", "__gwt_historyFrame")
  300. .attr("style",
  301. "position:absolute;width:0;height:0;border:0;overflow:hidden")
  302. .attr("src", "javascript:false"));
  303. String bootstrapLocation = staticFileLocation
  304. + "/VAADIN/vaadinBootstrap.js";
  305. fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type",
  306. "text/javascript").attr("src", bootstrapLocation));
  307. Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr(
  308. "type", "text/javascript");
  309. StringBuilder builder = new StringBuilder();
  310. builder.append("//<![CDATA[\n");
  311. builder.append("if (!window.vaadin) alert("
  312. + JSONObject.quote("Failed to load the bootstrap javascript: "
  313. + bootstrapLocation) + ");\n");
  314. appendMainScriptTagContents(context, builder);
  315. builder.append("//]]>");
  316. mainScriptTag.appendChild(new DataNode(builder.toString(),
  317. mainScriptTag.baseUri()));
  318. fragmentNodes.add(mainScriptTag);
  319. }
  320. protected void appendMainScriptTagContents(BootstrapContext context,
  321. StringBuilder builder) throws JSONException, IOException {
  322. JSONObject defaults = getDefaultParameters(context);
  323. JSONObject appConfig = getApplicationParameters(context);
  324. boolean isDebug = !context.getApplication().isProductionMode();
  325. builder.append("vaadin.setDefaults(");
  326. appendJsonObject(builder, defaults, isDebug);
  327. builder.append(");\n");
  328. builder.append("vaadin.initApplication(\"");
  329. builder.append(context.getAppId());
  330. builder.append("\",");
  331. appendJsonObject(builder, appConfig, isDebug);
  332. builder.append(");\n");
  333. }
  334. private static void appendJsonObject(StringBuilder builder,
  335. JSONObject jsonObject, boolean isDebug) throws JSONException {
  336. if (isDebug) {
  337. builder.append(jsonObject.toString(4));
  338. } else {
  339. builder.append(jsonObject.toString());
  340. }
  341. }
  342. protected JSONObject getApplicationParameters(BootstrapContext context)
  343. throws JSONException, PaintException {
  344. Application application = context.getApplication();
  345. Integer uiId = context.getUIId();
  346. JSONObject appConfig = new JSONObject();
  347. if (uiId != null) {
  348. appConfig.put(UIConstants.UI_ID_PARAMETER, uiId);
  349. }
  350. if (context.getThemeName() != null) {
  351. appConfig.put("themeUri",
  352. getThemeUri(context, context.getThemeName()));
  353. }
  354. JSONObject versionInfo = new JSONObject();
  355. versionInfo.put("vaadinVersion", Version.getFullVersion());
  356. versionInfo.put("applicationVersion", application.getVersion());
  357. appConfig.put("versionInfo", versionInfo);
  358. appConfig.put("widgetset", context.getWidgetsetName());
  359. if (uiId == null || application.isUIInitPending(uiId.intValue())) {
  360. appConfig.put("initialPath", context.getRequest()
  361. .getRequestPathInfo());
  362. Map<String, String[]> parameterMap = context.getRequest()
  363. .getParameterMap();
  364. appConfig.put("initialParams", parameterMap);
  365. } else {
  366. // write the initial UIDL into the config
  367. appConfig.put("uidl",
  368. getInitialUIDL(context.getRequest(), context.getUI()));
  369. }
  370. return appConfig;
  371. }
  372. protected JSONObject getDefaultParameters(BootstrapContext context)
  373. throws JSONException {
  374. JSONObject defaults = new JSONObject();
  375. WrappedRequest request = context.getRequest();
  376. Application application = context.getApplication();
  377. // Get system messages
  378. Application.SystemMessages systemMessages = AbstractApplicationServlet
  379. .getSystemMessages(application.getClass());
  380. if (systemMessages != null) {
  381. // Write the CommunicationError -message to client
  382. JSONObject comErrMsg = new JSONObject();
  383. comErrMsg.put("caption",
  384. systemMessages.getCommunicationErrorCaption());
  385. comErrMsg.put("message",
  386. systemMessages.getCommunicationErrorMessage());
  387. comErrMsg.put("url", systemMessages.getCommunicationErrorURL());
  388. defaults.put("comErrMsg", comErrMsg);
  389. JSONObject authErrMsg = new JSONObject();
  390. authErrMsg.put("caption",
  391. systemMessages.getAuthenticationErrorCaption());
  392. authErrMsg.put("message",
  393. systemMessages.getAuthenticationErrorMessage());
  394. authErrMsg.put("url", systemMessages.getAuthenticationErrorURL());
  395. defaults.put("authErrMsg", authErrMsg);
  396. }
  397. DeploymentConfiguration deploymentConfiguration = request
  398. .getDeploymentConfiguration();
  399. String staticFileLocation = deploymentConfiguration
  400. .getStaticFileLocation(request);
  401. String widgetsetBase = staticFileLocation + "/"
  402. + AbstractApplicationServlet.WIDGETSET_DIRECTORY_PATH;
  403. defaults.put("widgetsetBase", widgetsetBase);
  404. if (!application.isProductionMode()) {
  405. defaults.put("debug", true);
  406. }
  407. if (deploymentConfiguration.isStandalone(request)) {
  408. defaults.put("standalone", true);
  409. }
  410. defaults.put("heartbeatInterval",
  411. deploymentConfiguration.getHeartbeatInterval());
  412. defaults.put("appUri", getAppUri(context));
  413. return defaults;
  414. }
  415. protected abstract String getAppUri(BootstrapContext context);
  416. /**
  417. * Get the URI for the application theme.
  418. *
  419. * A portal-wide default theme is fetched from the portal shared resource
  420. * directory (if any), other themes from the portlet.
  421. *
  422. * @param context
  423. * @param themeName
  424. *
  425. * @return
  426. */
  427. public String getThemeUri(BootstrapContext context, String themeName) {
  428. WrappedRequest request = context.getRequest();
  429. final String staticFilePath = request.getDeploymentConfiguration()
  430. .getStaticFileLocation(request);
  431. return staticFilePath + "/"
  432. + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName;
  433. }
  434. /**
  435. * Override if required
  436. *
  437. * @param context
  438. * @return
  439. */
  440. public String getThemeName(BootstrapContext context) {
  441. return context.getApplication().getThemeForUI(context.getUI());
  442. }
  443. /**
  444. * Don not override.
  445. *
  446. * @param context
  447. * @return
  448. */
  449. public String findAndEscapeThemeName(BootstrapContext context) {
  450. String themeName = getThemeName(context);
  451. if (themeName == null) {
  452. WrappedRequest request = context.getRequest();
  453. themeName = request.getDeploymentConfiguration()
  454. .getConfiguredTheme(request);
  455. }
  456. // XSS preventation, theme names shouldn't contain special chars anyway.
  457. // The servlet denies them via url parameter.
  458. themeName = AbstractApplicationServlet.stripSpecialChars(themeName);
  459. return themeName;
  460. }
  461. protected void writeError(WrappedResponse response, Throwable e)
  462. throws IOException {
  463. response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
  464. e.getLocalizedMessage());
  465. }
  466. /**
  467. * Gets the initial UIDL message to send to the client.
  468. *
  469. * @param request
  470. * the originating request
  471. * @param ui
  472. * the UI for which the UIDL should be generated
  473. * @return a string with the initial UIDL message
  474. * @throws PaintException
  475. * if an exception occurs while painting the components
  476. * @throws JSONException
  477. * if an exception occurs while formatting the output
  478. */
  479. protected abstract String getInitialUIDL(WrappedRequest request, UI ui)
  480. throws PaintException, JSONException;
  481. }