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.

FileUploadHandler.java 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. /*
  2. * Copyright 2000-2018 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.communication;
  17. import java.io.BufferedWriter;
  18. import java.io.ByteArrayOutputStream;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.OutputStream;
  22. import java.io.OutputStreamWriter;
  23. import java.io.PrintWriter;
  24. import com.vaadin.server.ClientConnector;
  25. import com.vaadin.server.NoInputStreamException;
  26. import com.vaadin.server.NoOutputStreamException;
  27. import com.vaadin.server.RequestHandler;
  28. import com.vaadin.server.ServletPortletHelper;
  29. import com.vaadin.server.StreamVariable;
  30. import com.vaadin.server.StreamVariable.StreamingEndEvent;
  31. import com.vaadin.server.StreamVariable.StreamingErrorEvent;
  32. import com.vaadin.server.UploadException;
  33. import com.vaadin.server.VaadinRequest;
  34. import com.vaadin.server.VaadinResponse;
  35. import com.vaadin.server.VaadinSession;
  36. import com.vaadin.shared.ApplicationConstants;
  37. import com.vaadin.ui.UI;
  38. import com.vaadin.ui.Upload.FailedEvent;
  39. import static java.nio.charset.StandardCharsets.UTF_8;
  40. /**
  41. * Handles a file upload request submitted via an Upload component.
  42. *
  43. * @author Vaadin Ltd
  44. * @since 7.1
  45. */
  46. public class FileUploadHandler implements RequestHandler {
  47. public static final int MULTIPART_BOUNDARY_LINE_LIMIT = 20000;
  48. /**
  49. * Stream that extracts content from another stream until the boundary
  50. * string is encountered.
  51. *
  52. * Public only for unit tests, should be considered private for all other
  53. * purposes.
  54. */
  55. public static class SimpleMultiPartInputStream extends InputStream {
  56. /**
  57. * Counter of how many characters have been matched to boundary string
  58. * from the stream
  59. */
  60. int matchedCount = -1;
  61. /**
  62. * Used as pointer when returning bytes after partly matched boundary
  63. * string.
  64. */
  65. int curBoundaryIndex = 0;
  66. /**
  67. * The byte found after a "promising start for boundary"
  68. */
  69. private int bufferedByte = -1;
  70. private boolean atTheEnd = false;
  71. private final char[] boundary;
  72. private final InputStream realInputStream;
  73. public SimpleMultiPartInputStream(InputStream realInputStream,
  74. String boundaryString) {
  75. boundary = (CRLF + DASHDASH + boundaryString).toCharArray();
  76. this.realInputStream = realInputStream;
  77. }
  78. @Override
  79. public int read() throws IOException {
  80. if (atTheEnd) {
  81. // End boundary reached, nothing more to read
  82. return -1;
  83. } else if (bufferedByte >= 0) {
  84. /* Purge partially matched boundary if there was such */
  85. return getBuffered();
  86. } else if (matchedCount != -1) {
  87. /*
  88. * Special case where last "failed" matching ended with first
  89. * character from boundary string
  90. */
  91. return matchForBoundary();
  92. } else {
  93. int fromActualStream = realInputStream.read();
  94. if (fromActualStream == -1) {
  95. // unexpected end of stream
  96. throw new IOException(
  97. "The multipart stream ended unexpectedly");
  98. }
  99. if (boundary[0] == fromActualStream) {
  100. /*
  101. * If matches the first character in boundary string, start
  102. * checking if the boundary is fetched.
  103. */
  104. return matchForBoundary();
  105. }
  106. return fromActualStream;
  107. }
  108. }
  109. /**
  110. * Reads the input to expect a boundary string. Expects that the first
  111. * character has already been matched.
  112. *
  113. * @return -1 if the boundary was matched, else returns the first byte
  114. * from boundary
  115. * @throws IOException
  116. */
  117. private int matchForBoundary() throws IOException {
  118. matchedCount = 0;
  119. /*
  120. * Going to "buffered mode". Read until full boundary match or a
  121. * different character.
  122. */
  123. while (true) {
  124. matchedCount++;
  125. if (matchedCount == boundary.length) {
  126. /*
  127. * The whole boundary matched so we have reached the end of
  128. * file
  129. */
  130. atTheEnd = true;
  131. return -1;
  132. }
  133. int fromActualStream = realInputStream.read();
  134. if (fromActualStream != boundary[matchedCount]) {
  135. /*
  136. * Did not find full boundary, cache the mismatching byte
  137. * and start returning the partially matched boundary.
  138. */
  139. bufferedByte = fromActualStream;
  140. return getBuffered();
  141. }
  142. }
  143. }
  144. /**
  145. * Returns the partly matched boundary string and the byte following
  146. * that.
  147. *
  148. * @return
  149. * @throws IOException
  150. */
  151. private int getBuffered() throws IOException {
  152. int b;
  153. if (matchedCount == 0) {
  154. // The boundary has been returned, return the buffered byte.
  155. b = bufferedByte;
  156. bufferedByte = -1;
  157. matchedCount = -1;
  158. } else {
  159. b = boundary[curBoundaryIndex++];
  160. if (curBoundaryIndex == matchedCount) {
  161. // The full boundary has been returned, remaining is the
  162. // char that did not match the boundary.
  163. curBoundaryIndex = 0;
  164. if (bufferedByte != boundary[0]) {
  165. /*
  166. * next call for getBuffered will return the
  167. * bufferedByte that came after the partial boundary
  168. * match
  169. */
  170. matchedCount = 0;
  171. } else {
  172. /*
  173. * Special case where buffered byte again matches the
  174. * boundaryString. This could be the start of the real
  175. * end boundary.
  176. */
  177. matchedCount = 0;
  178. bufferedByte = -1;
  179. }
  180. }
  181. }
  182. if (b == -1) {
  183. throw new IOException(
  184. "The multipart stream ended unexpectedly");
  185. }
  186. return b;
  187. }
  188. }
  189. /**
  190. * An UploadInterruptedException will be thrown by an ongoing upload if
  191. * {@link StreamVariable#isInterrupted()} returns <code>true</code>.
  192. *
  193. * By checking the exception of an {@link StreamingErrorEvent} or
  194. * {@link FailedEvent} against this class, it is possible to determine if an
  195. * upload was interrupted by code or aborted due to any other exception.
  196. */
  197. public static class UploadInterruptedException extends Exception {
  198. /**
  199. * Constructs an instance of <code>UploadInterruptedException</code>.
  200. */
  201. public UploadInterruptedException() {
  202. super("Upload interrupted by other thread");
  203. }
  204. }
  205. /**
  206. * as per RFC 2045, line delimiters in headers are always CRLF, i.e. 13 10
  207. */
  208. private static final int LF = 10;
  209. private static final String CRLF = "\r\n";
  210. private static final String DASHDASH = "--";
  211. /*
  212. * Same as in apache commons file upload library that was previously used.
  213. */
  214. private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024;
  215. /* Minimum interval which will be used for streaming progress events. */
  216. public static final int DEFAULT_STREAMING_PROGRESS_EVENT_INTERVAL_MS = 500;
  217. @Override
  218. public boolean handleRequest(VaadinSession session, VaadinRequest request,
  219. VaadinResponse response) throws IOException {
  220. if (!ServletPortletHelper.isFileUploadRequest(request)) {
  221. return false;
  222. }
  223. /*
  224. * URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See
  225. * #createReceiverUrl
  226. */
  227. String pathInfo = request.getPathInfo();
  228. // strip away part until the data we are interested starts
  229. int startOfData = pathInfo
  230. .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX)
  231. + ServletPortletHelper.UPLOAD_URL_PREFIX.length();
  232. String uppUri = pathInfo.substring(startOfData);
  233. // 0= UIid, 1= cid, 2= name, 3= sec key
  234. String[] parts = uppUri.split("/", 4);
  235. String uiId = parts[0];
  236. String connectorId = parts[1];
  237. String variableName = parts[2];
  238. // These are retrieved while session is locked
  239. ClientConnector source;
  240. StreamVariable streamVariable;
  241. session.lock();
  242. try {
  243. UI uI = session.getUIById(Integer.parseInt(uiId));
  244. UI.setCurrent(uI);
  245. streamVariable = uI.getConnectorTracker()
  246. .getStreamVariable(connectorId, variableName);
  247. String secKey = uI.getConnectorTracker().getSeckey(streamVariable);
  248. if (secKey == null || !secKey.equals(parts[3])) {
  249. // TODO Should rethink error handling
  250. return true;
  251. }
  252. source = uI.getConnectorTracker().getConnector(connectorId);
  253. } finally {
  254. session.unlock();
  255. }
  256. String contentType = request.getContentType();
  257. if (contentType.contains("boundary")) {
  258. // Multipart requests contain boundary string
  259. doHandleSimpleMultipartFileUpload(session, request, response,
  260. streamVariable, variableName, source,
  261. contentType.split("boundary=")[1]);
  262. } else {
  263. // if boundary string does not exist, the posted file is from
  264. // XHR2.post(File)
  265. doHandleXhrFilePost(session, request, response, streamVariable,
  266. variableName, source, getContentLength(request));
  267. }
  268. return true;
  269. }
  270. private static String readLine(InputStream stream) throws IOException {
  271. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  272. int readByte = stream.read();
  273. while (readByte != LF) {
  274. if (readByte == -1) {
  275. throw new IOException(
  276. "The multipart stream ended unexpectedly");
  277. }
  278. bout.write(readByte);
  279. if (bout.size() > MULTIPART_BOUNDARY_LINE_LIMIT) {
  280. throw new IOException(
  281. "The multipart stream does not contain boundary");
  282. }
  283. readByte = stream.read();
  284. }
  285. byte[] bytes = bout.toByteArray();
  286. return new String(bytes, 0, bytes.length - 1, UTF_8);
  287. }
  288. /**
  289. * Method used to stream content from a multipart request (either from
  290. * servlet or portlet request) to given StreamVariable.
  291. * <p>
  292. * This method takes care of locking the session as needed and does not
  293. * assume the caller has locked the session. This allows the session to be
  294. * locked only when needed and not when handling the upload data.
  295. * </p>
  296. *
  297. * @param session
  298. * The session containing the stream variable
  299. * @param request
  300. * The upload request
  301. * @param response
  302. * The upload response
  303. * @param streamVariable
  304. * The destination stream variable
  305. * @param variableName
  306. * The name of the destination stream variable
  307. * @param owner
  308. * The owner of the stream variable
  309. * @param boundary
  310. * The mime boundary used in the upload request
  311. * @throws IOException
  312. * If there is a problem reading the request or writing the
  313. * response
  314. */
  315. protected void doHandleSimpleMultipartFileUpload(VaadinSession session,
  316. VaadinRequest request, VaadinResponse response,
  317. StreamVariable streamVariable, String variableName,
  318. ClientConnector owner, String boundary) throws IOException {
  319. // multipart parsing, supports only one file for request, but that is
  320. // fine for our current terminal
  321. final InputStream inputStream = request.getInputStream();
  322. long contentLength = getContentLength(request);
  323. boolean atStart = false;
  324. boolean firstFileFieldFound = false;
  325. String rawfilename = "unknown";
  326. String rawMimeType = "application/octet-stream";
  327. /*
  328. * Read the stream until the actual file starts (empty line). Read
  329. * filename and content type from multipart headers.
  330. */
  331. while (!atStart) {
  332. String readLine = readLine(inputStream);
  333. contentLength -= (readLine.getBytes(UTF_8).length + CRLF.length());
  334. if (readLine.startsWith("Content-Disposition:")
  335. && readLine.indexOf("filename=") > 0) {
  336. rawfilename = readLine.replaceAll(".*filename=", "");
  337. char quote = rawfilename.charAt(0);
  338. rawfilename = rawfilename.substring(1);
  339. rawfilename = rawfilename.substring(0,
  340. rawfilename.indexOf(quote));
  341. firstFileFieldFound = true;
  342. } else if (firstFileFieldFound && readLine.isEmpty()) {
  343. atStart = true;
  344. } else if (readLine.startsWith("Content-Type")) {
  345. rawMimeType = readLine.split(": ")[1];
  346. }
  347. }
  348. contentLength -= (boundary.length() + CRLF.length()
  349. + 2 * DASHDASH.length() + CRLF.length());
  350. /*
  351. * Reads bytes from the underlying stream. Compares the read bytes to
  352. * the boundary string and returns -1 if met.
  353. *
  354. * The matching happens so that if the read byte equals to the first
  355. * char of boundary string, the stream goes to "buffering mode". In
  356. * buffering mode bytes are read until the character does not match the
  357. * corresponding from boundary string or the full boundary string is
  358. * found.
  359. *
  360. * Note, if this is someday needed elsewhere, don't shoot yourself to
  361. * foot and split to a top level helper class.
  362. */
  363. InputStream simpleMultiPartReader = new SimpleMultiPartInputStream(
  364. inputStream, boundary);
  365. /*
  366. * Should report only the filename even if the browser sends the path
  367. */
  368. final String filename = removePath(rawfilename);
  369. final String mimeType = rawMimeType;
  370. try {
  371. handleFileUploadValidationAndData(session, simpleMultiPartReader,
  372. streamVariable, filename, mimeType, contentLength, owner,
  373. variableName);
  374. } catch (UploadException e) {
  375. session.getCommunicationManager()
  376. .handleConnectorRelatedException(owner, e);
  377. }
  378. sendUploadResponse(request, response);
  379. }
  380. /*
  381. * request.getContentLength() is limited to "int" by the Servlet
  382. * specification. To support larger file uploads manually evaluate the
  383. * Content-Length header which can contain long values.
  384. */
  385. private long getContentLength(VaadinRequest request) {
  386. try {
  387. return Long.parseLong(request.getHeader("Content-Length"));
  388. } catch (NumberFormatException e) {
  389. return -1;
  390. }
  391. }
  392. private void handleFileUploadValidationAndData(VaadinSession session,
  393. InputStream inputStream, StreamVariable streamVariable,
  394. String filename, String mimeType, long contentLength,
  395. ClientConnector connector, String variableName)
  396. throws UploadException {
  397. session.lock();
  398. try {
  399. if (connector == null) {
  400. throw new UploadException(
  401. "File upload ignored because the connector for the stream variable was not found");
  402. }
  403. if (!connector.isConnectorEnabled()) {
  404. throw new UploadException("Warning: file upload ignored for "
  405. + connector.getConnectorId()
  406. + " because the component was disabled");
  407. }
  408. } finally {
  409. session.unlock();
  410. }
  411. try {
  412. // Store ui reference so we can do cleanup even if connector is
  413. // detached in some event handler
  414. UI ui = connector.getUI();
  415. boolean forgetVariable = streamToReceiver(session, inputStream,
  416. streamVariable, filename, mimeType, contentLength);
  417. if (forgetVariable) {
  418. cleanStreamVariable(session, ui, connector, variableName);
  419. }
  420. } catch (Exception e) {
  421. session.lock();
  422. try {
  423. session.getCommunicationManager()
  424. .handleConnectorRelatedException(connector, e);
  425. } finally {
  426. session.unlock();
  427. }
  428. }
  429. }
  430. /**
  431. * Used to stream plain file post (aka XHR2.post(File))
  432. * <p>
  433. * This method takes care of locking the session as needed and does not
  434. * assume the caller has locked the session. This allows the session to be
  435. * locked only when needed and not when handling the upload data.
  436. * </p>
  437. *
  438. * @param session
  439. * The session containing the stream variable
  440. * @param request
  441. * The upload request
  442. * @param response
  443. * The upload response
  444. * @param streamVariable
  445. * The destination stream variable
  446. * @param variableName
  447. * The name of the destination stream variable
  448. * @param owner
  449. * The owner of the stream variable
  450. * @param contentLength
  451. * The length of the request content
  452. * @throws IOException
  453. * If there is a problem reading the request or writing the
  454. * response
  455. */
  456. protected void doHandleXhrFilePost(VaadinSession session,
  457. VaadinRequest request, VaadinResponse response,
  458. StreamVariable streamVariable, String variableName,
  459. ClientConnector owner, long contentLength) throws IOException {
  460. // These are unknown in filexhr ATM, maybe add to Accept header that
  461. // is accessible in portlets
  462. final String filename = "unknown";
  463. final String mimeType = filename;
  464. final InputStream stream = request.getInputStream();
  465. try {
  466. handleFileUploadValidationAndData(session, stream, streamVariable,
  467. filename, mimeType, contentLength, owner, variableName);
  468. } catch (UploadException e) {
  469. session.getCommunicationManager()
  470. .handleConnectorRelatedException(owner, e);
  471. }
  472. sendUploadResponse(request, response);
  473. }
  474. /**
  475. * @param in
  476. * @param streamVariable
  477. * @param filename
  478. * @param type
  479. * @param contentLength
  480. * @return true if the streamvariable has informed that the terminal can
  481. * forget this variable
  482. * @throws UploadException
  483. */
  484. protected final boolean streamToReceiver(VaadinSession session,
  485. final InputStream in, StreamVariable streamVariable,
  486. String filename, String type, long contentLength)
  487. throws UploadException {
  488. if (streamVariable == null) {
  489. throw new IllegalStateException(
  490. "StreamVariable for the post not found");
  491. }
  492. OutputStream out = null;
  493. long totalBytes = 0;
  494. StreamingStartEventImpl startedEvent = new StreamingStartEventImpl(
  495. filename, type, contentLength);
  496. try {
  497. boolean listenProgress;
  498. session.lock();
  499. try {
  500. streamVariable.streamingStarted(startedEvent);
  501. out = streamVariable.getOutputStream();
  502. listenProgress = streamVariable.listenProgress();
  503. } finally {
  504. session.unlock();
  505. }
  506. // Gets the output target stream
  507. if (out == null) {
  508. throw new NoOutputStreamException();
  509. }
  510. if (null == in) {
  511. // No file, for instance non-existent filename in html upload
  512. throw new NoInputStreamException();
  513. }
  514. final byte[] buffer = new byte[MAX_UPLOAD_BUFFER_SIZE];
  515. long lastStreamingEvent = 0;
  516. int bytesReadToBuffer = 0;
  517. do {
  518. bytesReadToBuffer = in.read(buffer);
  519. if (bytesReadToBuffer > 0) {
  520. out.write(buffer, 0, bytesReadToBuffer);
  521. totalBytes += bytesReadToBuffer;
  522. }
  523. if (listenProgress) {
  524. long now = System.currentTimeMillis();
  525. // to avoid excessive session locking and event storms,
  526. // events are sent in intervals, or at the end of the file.
  527. if (lastStreamingEvent + getProgressEventInterval() <= now
  528. || bytesReadToBuffer <= 0) {
  529. lastStreamingEvent = now;
  530. session.lock();
  531. try {
  532. StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl(
  533. filename, type, contentLength, totalBytes);
  534. streamVariable.onProgress(progressEvent);
  535. } finally {
  536. session.unlock();
  537. }
  538. }
  539. }
  540. if (streamVariable.isInterrupted()) {
  541. throw new UploadInterruptedException();
  542. }
  543. } while (bytesReadToBuffer > 0);
  544. // upload successful
  545. out.close();
  546. StreamingEndEvent event = new StreamingEndEventImpl(filename, type,
  547. totalBytes);
  548. session.lock();
  549. try {
  550. streamVariable.streamingFinished(event);
  551. } finally {
  552. session.unlock();
  553. }
  554. } catch (UploadInterruptedException e) {
  555. // Download interrupted by application code
  556. tryToCloseStream(out);
  557. StreamingErrorEvent event = new StreamingErrorEventImpl(filename,
  558. type, contentLength, totalBytes, e);
  559. session.lock();
  560. try {
  561. streamVariable.streamingFailed(event);
  562. } finally {
  563. session.unlock();
  564. }
  565. boolean pushEnabled = UI.getCurrent().getPushConfiguration()
  566. .getPushMode().isEnabled();
  567. if (!pushEnabled) {
  568. return true;
  569. }
  570. // Note, we are not throwing interrupted exception forward as it is
  571. // not a terminal level error like all other exception.
  572. } catch (final Exception e) {
  573. tryToCloseStream(out);
  574. session.lock();
  575. try {
  576. StreamingErrorEvent event = new StreamingErrorEventImpl(
  577. filename, type, contentLength, totalBytes, e);
  578. streamVariable.streamingFailed(event);
  579. // throw exception for terminal to be handled (to be passed to
  580. // terminalErrorHandler)
  581. throw new UploadException(e);
  582. } finally {
  583. session.unlock();
  584. }
  585. }
  586. return startedEvent.isDisposed();
  587. }
  588. /**
  589. * To prevent event storming, streaming progress events are sent in this
  590. * interval rather than every time the buffer is filled. This fixes #13155.
  591. * To adjust this value override the method, and register your own handler
  592. * in VaadinService.createRequestHandlers(). The default is 500ms, and
  593. * setting it to 0 effectively restores the old behavior.
  594. */
  595. protected int getProgressEventInterval() {
  596. return DEFAULT_STREAMING_PROGRESS_EVENT_INTERVAL_MS;
  597. }
  598. static void tryToCloseStream(OutputStream out) {
  599. try {
  600. // try to close output stream (e.g. file handle)
  601. if (out != null) {
  602. out.close();
  603. }
  604. } catch (IOException e1) {
  605. // NOP
  606. }
  607. }
  608. /**
  609. * Removes any possible path information from the filename and returns the
  610. * filename. Separators / and \\ are used.
  611. *
  612. * @param filename
  613. * @return
  614. */
  615. private static String removePath(String filename) {
  616. if (filename != null) {
  617. filename = filename.replaceAll("^.*[/\\\\]", "");
  618. }
  619. return filename;
  620. }
  621. /**
  622. * Sends the upload response.
  623. *
  624. * @param request
  625. * @param response
  626. * @throws IOException
  627. */
  628. protected void sendUploadResponse(VaadinRequest request,
  629. VaadinResponse response) throws IOException {
  630. response.setContentType(
  631. ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8);
  632. try (OutputStream out = response.getOutputStream()) {
  633. final PrintWriter outWriter = new PrintWriter(
  634. new BufferedWriter(new OutputStreamWriter(out, UTF_8)));
  635. outWriter.print("<html><body>download handled</body></html>");
  636. outWriter.flush();
  637. }
  638. }
  639. private void cleanStreamVariable(VaadinSession session, final UI ui,
  640. final ClientConnector owner, final String variableName) {
  641. session.accessSynchronously(() -> ui.getConnectorTracker()
  642. .cleanStreamVariable(owner.getConnectorId(), variableName));
  643. }
  644. }