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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. /*
  2. * Copyright 2000-2016 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.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. /**
  40. * Handles a file upload request submitted via an Upload component.
  41. *
  42. * @author Vaadin Ltd
  43. * @since 7.1
  44. */
  45. public class FileUploadHandler implements RequestHandler {
  46. public static final int MULTIPART_BOUNDARY_LINE_LIMIT = 20000;
  47. /**
  48. * Stream that extracts content from another stream until the boundary
  49. * string is encountered.
  50. *
  51. * Public only for unit tests, should be considered private for all other
  52. * purposes.
  53. */
  54. public static class SimpleMultiPartInputStream extends InputStream {
  55. /**
  56. * Counter of how many characters have been matched to boundary string
  57. * from the stream
  58. */
  59. int matchedCount = -1;
  60. /**
  61. * Used as pointer when returning bytes after partly matched boundary
  62. * string.
  63. */
  64. int curBoundaryIndex = 0;
  65. /**
  66. * The byte found after a "promising start for boundary"
  67. */
  68. private int bufferedByte = -1;
  69. private boolean atTheEnd = false;
  70. private final char[] boundary;
  71. private final InputStream realInputStream;
  72. public SimpleMultiPartInputStream(InputStream realInputStream,
  73. String boundaryString) {
  74. boundary = (CRLF + DASHDASH + boundaryString).toCharArray();
  75. this.realInputStream = realInputStream;
  76. }
  77. @Override
  78. public int read() throws IOException {
  79. if (atTheEnd) {
  80. // End boundary reached, nothing more to read
  81. return -1;
  82. } else if (bufferedByte >= 0) {
  83. /* Purge partially matched boundary if there was such */
  84. return getBuffered();
  85. } else if (matchedCount != -1) {
  86. /*
  87. * Special case where last "failed" matching ended with first
  88. * character from boundary string
  89. */
  90. return matchForBoundary();
  91. } else {
  92. int fromActualStream = realInputStream.read();
  93. if (fromActualStream == -1) {
  94. // unexpected end of stream
  95. throw new IOException(
  96. "The multipart stream ended unexpectedly");
  97. }
  98. if (boundary[0] == fromActualStream) {
  99. /*
  100. * If matches the first character in boundary string, start
  101. * checking if the boundary is fetched.
  102. */
  103. return matchForBoundary();
  104. }
  105. return fromActualStream;
  106. }
  107. }
  108. /**
  109. * Reads the input to expect a boundary string. Expects that the first
  110. * character has already been matched.
  111. *
  112. * @return -1 if the boundary was matched, else returns the first byte
  113. * from boundary
  114. * @throws IOException
  115. */
  116. private int matchForBoundary() throws IOException {
  117. matchedCount = 0;
  118. /*
  119. * Going to "buffered mode". Read until full boundary match or a
  120. * different character.
  121. */
  122. while (true) {
  123. matchedCount++;
  124. if (matchedCount == boundary.length) {
  125. /*
  126. * The whole boundary matched so we have reached the end of
  127. * file
  128. */
  129. atTheEnd = true;
  130. return -1;
  131. }
  132. int fromActualStream = realInputStream.read();
  133. if (fromActualStream != boundary[matchedCount]) {
  134. /*
  135. * Did not find full boundary, cache the mismatching byte
  136. * and start returning the partially matched boundary.
  137. */
  138. bufferedByte = fromActualStream;
  139. return getBuffered();
  140. }
  141. }
  142. }
  143. /**
  144. * Returns the partly matched boundary string and the byte following
  145. * that.
  146. *
  147. * @return
  148. * @throws IOException
  149. */
  150. private int getBuffered() throws IOException {
  151. int b;
  152. if (matchedCount == 0) {
  153. // The boundary has been returned, return the buffered byte.
  154. b = bufferedByte;
  155. bufferedByte = -1;
  156. matchedCount = -1;
  157. } else {
  158. b = boundary[curBoundaryIndex++];
  159. if (curBoundaryIndex == matchedCount) {
  160. // The full boundary has been returned, remaining is the
  161. // char that did not match the boundary.
  162. curBoundaryIndex = 0;
  163. if (bufferedByte != boundary[0]) {
  164. /*
  165. * next call for getBuffered will return the
  166. * bufferedByte that came after the partial boundary
  167. * match
  168. */
  169. matchedCount = 0;
  170. } else {
  171. /*
  172. * Special case where buffered byte again matches the
  173. * boundaryString. This could be the start of the real
  174. * end boundary.
  175. */
  176. matchedCount = 0;
  177. bufferedByte = -1;
  178. }
  179. }
  180. }
  181. if (b == -1) {
  182. throw new IOException(
  183. "The multipart stream ended unexpectedly");
  184. }
  185. return b;
  186. }
  187. }
  188. /**
  189. * An UploadInterruptedException will be thrown by an ongoing upload if
  190. * {@link StreamVariable#isInterrupted()} returns <code>true</code>.
  191. *
  192. * By checking the exception of an {@link StreamingErrorEvent} or
  193. * {@link FailedEvent} against this class, it is possible to determine if an
  194. * upload was interrupted by code or aborted due to any other exception.
  195. */
  196. public static class UploadInterruptedException extends Exception {
  197. /**
  198. * Constructs an instance of <code>UploadInterruptedException</code>.
  199. */
  200. public UploadInterruptedException() {
  201. super("Upload interrupted by other thread");
  202. }
  203. }
  204. /**
  205. * as per RFC 2045, line delimiters in headers are always CRLF, i.e. 13 10
  206. */
  207. private static final int LF = 10;
  208. private static final String CRLF = "\r\n";
  209. private static final String UTF8 = "UTF-8";
  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. String[] parts = uppUri.split("/", 4); // 0= UIid, 1 = cid, 2= name, 3
  234. // = sec key
  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, UTF8);
  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(UTF8).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 -1l;
  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. // Note, we are not throwing interrupted exception forward as it is
  566. // not a terminal level error like all other exception.
  567. } catch (final Exception e) {
  568. tryToCloseStream(out);
  569. session.lock();
  570. try {
  571. StreamingErrorEvent event = new StreamingErrorEventImpl(
  572. filename, type, contentLength, totalBytes, e);
  573. streamVariable.streamingFailed(event);
  574. // throw exception for terminal to be handled (to be passed to
  575. // terminalErrorHandler)
  576. throw new UploadException(e);
  577. } finally {
  578. session.unlock();
  579. }
  580. }
  581. return startedEvent.isDisposed();
  582. }
  583. /**
  584. * To prevent event storming, streaming progress events are sent in this
  585. * interval rather than every time the buffer is filled. This fixes #13155.
  586. * To adjust this value override the method, and register your own handler
  587. * in VaadinService.createRequestHandlers(). The default is 500ms, and
  588. * setting it to 0 effectively restores the old behavior.
  589. */
  590. protected int getProgressEventInterval() {
  591. return DEFAULT_STREAMING_PROGRESS_EVENT_INTERVAL_MS;
  592. }
  593. static void tryToCloseStream(OutputStream out) {
  594. try {
  595. // try to close output stream (e.g. file handle)
  596. if (out != null) {
  597. out.close();
  598. }
  599. } catch (IOException e1) {
  600. // NOP
  601. }
  602. }
  603. /**
  604. * Removes any possible path information from the filename and returns the
  605. * filename. Separators / and \\ are used.
  606. *
  607. * @param filename
  608. * @return
  609. */
  610. private static String removePath(String filename) {
  611. if (filename != null) {
  612. filename = filename.replaceAll("^.*[/\\\\]", "");
  613. }
  614. return filename;
  615. }
  616. /**
  617. * TODO document
  618. *
  619. * @param request
  620. * @param response
  621. * @throws IOException
  622. */
  623. protected void sendUploadResponse(VaadinRequest request,
  624. VaadinResponse response) throws IOException {
  625. response.setContentType(
  626. ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8);
  627. try (OutputStream out = response.getOutputStream()) {
  628. final PrintWriter outWriter = new PrintWriter(
  629. new BufferedWriter(new OutputStreamWriter(out, "UTF-8")));
  630. outWriter.print("<html><body>download handled</body></html>");
  631. outWriter.flush();
  632. }
  633. }
  634. private void cleanStreamVariable(VaadinSession session, final UI ui,
  635. final ClientConnector owner, final String variableName) {
  636. session.accessSynchronously(() -> {
  637. ui.getConnectorTracker().cleanStreamVariable(owner.getConnectorId(),
  638. variableName);
  639. });
  640. }
  641. }