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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  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. if (uI == null) {
  245. throw new IOException(
  246. "File upload ignored because the UI was not found and stream variable cannot be determined");
  247. }
  248. // Set UI so that it can be used in stream variable clean up
  249. UI.setCurrent(uI);
  250. streamVariable = uI.getConnectorTracker()
  251. .getStreamVariable(connectorId, variableName);
  252. String secKey = uI.getConnectorTracker().getSeckey(streamVariable);
  253. if (secKey == null || !secKey.equals(parts[3])) {
  254. return true;
  255. }
  256. source = uI.getConnectorTracker().getConnector(connectorId);
  257. } finally {
  258. session.unlock();
  259. }
  260. String contentType = request.getContentType();
  261. if (contentType.contains("boundary")) {
  262. // Multipart requests contain boundary string
  263. doHandleSimpleMultipartFileUpload(session, request, response,
  264. streamVariable, variableName, source,
  265. contentType.split("boundary=")[1]);
  266. } else {
  267. // if boundary string does not exist, the posted file is from
  268. // XHR2.post(File)
  269. doHandleXhrFilePost(session, request, response, streamVariable,
  270. variableName, source, getContentLength(request));
  271. }
  272. return true;
  273. }
  274. private static String readLine(InputStream stream) throws IOException {
  275. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  276. int readByte = stream.read();
  277. while (readByte != LF) {
  278. if (readByte == -1) {
  279. throw new IOException(
  280. "The multipart stream ended unexpectedly");
  281. }
  282. bout.write(readByte);
  283. if (bout.size() > MULTIPART_BOUNDARY_LINE_LIMIT) {
  284. throw new IOException(
  285. "The multipart stream does not contain boundary");
  286. }
  287. readByte = stream.read();
  288. }
  289. byte[] bytes = bout.toByteArray();
  290. return new String(bytes, 0, bytes.length - 1, UTF_8);
  291. }
  292. /**
  293. * Method used to stream content from a multipart request (either from
  294. * servlet or portlet request) to given StreamVariable.
  295. * <p>
  296. * This method takes care of locking the session as needed and does not
  297. * assume the caller has locked the session. This allows the session to be
  298. * locked only when needed and not when handling the upload data.
  299. * </p>
  300. *
  301. * @param session
  302. * The session containing the stream variable
  303. * @param request
  304. * The upload request
  305. * @param response
  306. * The upload response
  307. * @param streamVariable
  308. * The destination stream variable
  309. * @param variableName
  310. * The name of the destination stream variable
  311. * @param owner
  312. * The owner of the stream variable
  313. * @param boundary
  314. * The mime boundary used in the upload request
  315. * @throws IOException
  316. * If there is a problem reading the request or writing the
  317. * response
  318. */
  319. protected void doHandleSimpleMultipartFileUpload(VaadinSession session,
  320. VaadinRequest request, VaadinResponse response,
  321. StreamVariable streamVariable, String variableName,
  322. ClientConnector owner, String boundary) throws IOException {
  323. // multipart parsing, supports only one file for request, but that is
  324. // fine for our current terminal
  325. final InputStream inputStream = request.getInputStream();
  326. long contentLength = getContentLength(request);
  327. boolean atStart = false;
  328. boolean firstFileFieldFound = false;
  329. String rawfilename = "unknown";
  330. String rawMimeType = "application/octet-stream";
  331. /*
  332. * Read the stream until the actual file starts (empty line). Read
  333. * filename and content type from multipart headers.
  334. */
  335. while (!atStart) {
  336. String readLine = readLine(inputStream);
  337. contentLength -= (readLine.getBytes(UTF_8).length + CRLF.length());
  338. if (readLine.startsWith("Content-Disposition:")
  339. && readLine.indexOf("filename=") > 0) {
  340. rawfilename = readLine.replaceAll(".*filename=", "");
  341. char quote = rawfilename.charAt(0);
  342. rawfilename = rawfilename.substring(1);
  343. rawfilename = rawfilename.substring(0,
  344. rawfilename.indexOf(quote));
  345. firstFileFieldFound = true;
  346. } else if (firstFileFieldFound && readLine.isEmpty()) {
  347. atStart = true;
  348. } else if (readLine.startsWith("Content-Type")) {
  349. rawMimeType = readLine.split(": ")[1];
  350. }
  351. }
  352. contentLength -= (boundary.length() + CRLF.length()
  353. + 2 * DASHDASH.length() + CRLF.length());
  354. /*
  355. * Reads bytes from the underlying stream. Compares the read bytes to
  356. * the boundary string and returns -1 if met.
  357. *
  358. * The matching happens so that if the read byte equals to the first
  359. * char of boundary string, the stream goes to "buffering mode". In
  360. * buffering mode bytes are read until the character does not match the
  361. * corresponding from boundary string or the full boundary string is
  362. * found.
  363. *
  364. * Note, if this is someday needed elsewhere, don't shoot yourself to
  365. * foot and split to a top level helper class.
  366. */
  367. InputStream simpleMultiPartReader = new SimpleMultiPartInputStream(
  368. inputStream, boundary);
  369. /*
  370. * Should report only the filename even if the browser sends the path
  371. */
  372. final String filename = removePath(rawfilename);
  373. final String mimeType = rawMimeType;
  374. try {
  375. handleFileUploadValidationAndData(session, simpleMultiPartReader,
  376. streamVariable, filename, mimeType, contentLength, owner,
  377. variableName);
  378. } catch (UploadException e) {
  379. session.getCommunicationManager()
  380. .handleConnectorRelatedException(owner, e);
  381. }
  382. sendUploadResponse(request, response);
  383. }
  384. /*
  385. * request.getContentLength() is limited to "int" by the Servlet
  386. * specification. To support larger file uploads manually evaluate the
  387. * Content-Length header which can contain long values.
  388. */
  389. private long getContentLength(VaadinRequest request) {
  390. try {
  391. return Long.parseLong(request.getHeader("Content-Length"));
  392. } catch (NumberFormatException e) {
  393. return -1;
  394. }
  395. }
  396. private void handleFileUploadValidationAndData(VaadinSession session,
  397. InputStream inputStream, StreamVariable streamVariable,
  398. String filename, String mimeType, long contentLength,
  399. ClientConnector connector, String variableName)
  400. throws UploadException {
  401. session.lock();
  402. try {
  403. if (connector == null) {
  404. throw new UploadException(
  405. "File upload ignored because the connector for the stream variable was not found");
  406. }
  407. if (!connector.isConnectorEnabled()) {
  408. throw new UploadException("Warning: file upload ignored for "
  409. + connector.getConnectorId()
  410. + " because the component was disabled");
  411. }
  412. } finally {
  413. session.unlock();
  414. }
  415. try {
  416. // Store ui reference so we can do cleanup even if connector is
  417. // detached in some event handler
  418. UI ui = UI.getCurrent();
  419. boolean forgetVariable = streamToReceiver(session, inputStream,
  420. streamVariable, filename, mimeType, contentLength);
  421. if (forgetVariable) {
  422. cleanStreamVariable(session, ui, connector, variableName);
  423. }
  424. } catch (Exception e) {
  425. session.lock();
  426. try {
  427. session.getCommunicationManager()
  428. .handleConnectorRelatedException(connector, e);
  429. } finally {
  430. session.unlock();
  431. }
  432. }
  433. }
  434. /**
  435. * Used to stream plain file post (aka XHR2.post(File))
  436. * <p>
  437. * This method takes care of locking the session as needed and does not
  438. * assume the caller has locked the session. This allows the session to be
  439. * locked only when needed and not when handling the upload data.
  440. * </p>
  441. *
  442. * @param session
  443. * The session containing the stream variable
  444. * @param request
  445. * The upload request
  446. * @param response
  447. * The upload response
  448. * @param streamVariable
  449. * The destination stream variable
  450. * @param variableName
  451. * The name of the destination stream variable
  452. * @param owner
  453. * The owner of the stream variable
  454. * @param contentLength
  455. * The length of the request content
  456. * @throws IOException
  457. * If there is a problem reading the request or writing the
  458. * response
  459. */
  460. protected void doHandleXhrFilePost(VaadinSession session,
  461. VaadinRequest request, VaadinResponse response,
  462. StreamVariable streamVariable, String variableName,
  463. ClientConnector owner, long contentLength) throws IOException {
  464. // These are unknown in filexhr ATM, maybe add to Accept header that
  465. // is accessible in portlets
  466. final String filename = "unknown";
  467. final String mimeType = filename;
  468. final InputStream stream = request.getInputStream();
  469. try {
  470. handleFileUploadValidationAndData(session, stream, streamVariable,
  471. filename, mimeType, contentLength, owner, variableName);
  472. } catch (UploadException e) {
  473. session.getCommunicationManager()
  474. .handleConnectorRelatedException(owner, e);
  475. }
  476. sendUploadResponse(request, response);
  477. }
  478. /**
  479. * @param in
  480. * @param streamVariable
  481. * @param filename
  482. * @param type
  483. * @param contentLength
  484. * @return true if the streamvariable has informed that the terminal can
  485. * forget this variable
  486. * @throws UploadException
  487. */
  488. protected final boolean streamToReceiver(VaadinSession session,
  489. final InputStream in, StreamVariable streamVariable,
  490. String filename, String type, long contentLength)
  491. throws UploadException {
  492. if (streamVariable == null) {
  493. throw new IllegalStateException(
  494. "StreamVariable for the post not found");
  495. }
  496. OutputStream out = null;
  497. long totalBytes = 0;
  498. StreamingStartEventImpl startedEvent = new StreamingStartEventImpl(
  499. filename, type, contentLength);
  500. try {
  501. boolean listenProgress;
  502. session.lock();
  503. try {
  504. streamVariable.streamingStarted(startedEvent);
  505. out = streamVariable.getOutputStream();
  506. listenProgress = streamVariable.listenProgress();
  507. } finally {
  508. session.unlock();
  509. }
  510. // Gets the output target stream
  511. if (out == null) {
  512. throw new NoOutputStreamException();
  513. }
  514. if (null == in) {
  515. // No file, for instance non-existent filename in html upload
  516. throw new NoInputStreamException();
  517. }
  518. final byte[] buffer = new byte[MAX_UPLOAD_BUFFER_SIZE];
  519. long lastStreamingEvent = 0;
  520. int bytesReadToBuffer = 0;
  521. do {
  522. bytesReadToBuffer = in.read(buffer);
  523. if (bytesReadToBuffer > 0) {
  524. out.write(buffer, 0, bytesReadToBuffer);
  525. totalBytes += bytesReadToBuffer;
  526. }
  527. if (listenProgress) {
  528. long now = System.currentTimeMillis();
  529. // to avoid excessive session locking and event storms,
  530. // events are sent in intervals, or at the end of the file.
  531. if (lastStreamingEvent + getProgressEventInterval() <= now
  532. || bytesReadToBuffer <= 0) {
  533. lastStreamingEvent = now;
  534. session.lock();
  535. try {
  536. StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl(
  537. filename, type, contentLength, totalBytes);
  538. streamVariable.onProgress(progressEvent);
  539. } finally {
  540. session.unlock();
  541. }
  542. }
  543. }
  544. if (streamVariable.isInterrupted()) {
  545. throw new UploadInterruptedException();
  546. }
  547. } while (bytesReadToBuffer > 0);
  548. // upload successful
  549. out.close();
  550. StreamingEndEvent event = new StreamingEndEventImpl(filename, type,
  551. totalBytes);
  552. session.lock();
  553. try {
  554. streamVariable.streamingFinished(event);
  555. } finally {
  556. session.unlock();
  557. }
  558. } catch (UploadInterruptedException e) {
  559. // Download interrupted by application code
  560. tryToCloseStream(out);
  561. StreamingErrorEvent event = new StreamingErrorEventImpl(filename,
  562. type, contentLength, totalBytes, e);
  563. session.lock();
  564. try {
  565. streamVariable.streamingFailed(event);
  566. } finally {
  567. session.unlock();
  568. }
  569. return true;
  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(() -> {
  642. ui.getConnectorTracker().cleanStreamVariable(owner.getConnectorId(),
  643. variableName);
  644. // in case of automatic push mode, the client connector
  645. // could already have refreshed its StreamVariable
  646. // in the ConnectorTracker. For instance, the Upload component
  647. // adds its stream variable in its paintContent method, which is
  648. // called (indirectly) on each session unlock in case of automatic
  649. // pushes.
  650. // To cover this case, mark the client connector as dirty, so that
  651. // the unlock after this runnable refreshes the StreamVariable
  652. // again.
  653. owner.markAsDirty();
  654. });
  655. }
  656. }