1919 */
2020package com .flowingcode .vaadin .addons .gridexporter ;
2121
22- import com .vaadin .flow .component .UI ;
2322import com .vaadin .flow .server .streams .DownloadHandler ;
2423import com .vaadin .flow .server .streams .DownloadEvent ;
2524import com .vaadin .flow .server .VaadinSession ;
2625import java .io .IOException ;
2726import java .io .InterruptedIOException ;
28- import java .io .OutputStream ;
2927import java .nio .channels .InterruptedByTimeoutException ;
30- import java .util .Optional ;
31- import java .util .concurrent .Semaphore ;
32- import java .util .concurrent .TimeUnit ;
33- import java .util .function .IntFunction ;
3428
3529/**
3630 * An implementation of {@link DownloadHandler} that controls access to the
4135 * @author Javier Godoy
4236 */
4337@ SuppressWarnings ("serial" )
44- abstract class ConcurrentDownloadHandler implements DownloadHandler {
45-
46- public static final float MAX_COST = 0x7FFF ;
47-
48- public static final float MIN_COST = 1.0f / 0x10000 ;
49-
50- public static final float DEFAULT_COST = 1.0f ;
51-
52- private static final ConfigurableSemaphore semaphore = new ConfigurableSemaphore ();
53-
54- private static volatile boolean enabled ;
55-
56- private static volatile boolean failOnUiChange ;
38+ abstract class ConcurrentDownloadHandler extends ConcurrentOperationBase implements DownloadHandler {
5739
5840 private final DownloadHandler delegate ;
5941
60- private static final class ConfigurableSemaphore extends Semaphore {
61-
62- private int maxPermits ;
63-
64- ConfigurableSemaphore () {
65- super (0 );
66- }
67-
68- synchronized void setPermits (int permits ) {
69- if (permits < 0 ) {
70- throw new IllegalArgumentException ();
71- }
72- int delta = permits - maxPermits ;
73- if (delta > 0 ) {
74- super .release (delta );
75- } else if (delta < 0 ) {
76- super .reducePermits (-delta );
77- }
78- maxPermits = permits ;
79- }
80-
81- @ Override
82- public String toString () {
83- IntFunction <String > str = permits -> {
84- float f = permits / (float ) 0x10000 ;
85- return f == Math .floor (f ) ? String .format ("%.0f" , f ) : Float .toString (f );
86- };
87- return "Semaphore[" + str .apply (availablePermits ()) + "/" + str .apply (maxPermits ) + "]" ;
88- }
89-
90- }
91-
92- /**
93- * Sets the limit for the cost of concurrent downloads.
94- * <p>
95- * Finite limits are capped to {@link #MAX_COST} (32767). If the limit is
96- * {@link Float#POSITIVE_INFINITY POSITIVE_INFINITY}, the semaphore will not be
97- * used for
98- * controlling concurrent downloads.
99- *
100- * @param limit the maximum cost of concurrent downloads allowed
101- * @throws IllegalArgumentException if the limit is zero or negative.
102- */
103- public static void setLimit (float limit ) {
104- if (limit <= 0 ) {
105- throw new IllegalArgumentException ();
106- }
107- if (Float .isInfinite (limit )) {
108- enabled = false ;
109- return ;
110- }
111-
112- synchronized (semaphore ) {
113- enabled = true ;
114- semaphore .setPermits (costToPermits (limit , Integer .MAX_VALUE ));
115- }
116- }
117-
118- static void setFailOnUiChange (boolean failOnUiChange ) {
119- ConcurrentDownloadHandler .failOnUiChange = failOnUiChange ;
120- }
121-
122- /**
123- * Returns the limit for the number of concurrent downloads.
124- *
125- * @return the limit for the number of concurrent downloads, or
126- * {@link Float#POSITIVE_INFINITY} if
127- * the semaphore is disabled.
128- */
129- public static float getLimit () {
130- if (enabled ) {
131- synchronized (semaphore ) {
132- return (float ) semaphore .maxPermits / 0x10000 ;
133- }
134- } else {
135- return Float .POSITIVE_INFINITY ;
136- }
137- }
138-
139- private static int costToPermits (float cost , int maxPermits ) {
140- // restrict limit to 0x7fff to ensure the cost can be represented
141- // using fixed-point arithmetic with 16 fractional digits and 15 integral digits
142- cost = Math .min (cost , MAX_COST );
143- // Determine the number of permits required based on the cost, capping at
144- // maxPermits.
145- // If the cost is zero or negative, no permits are needed.
146- // Any positive cost, no matter how small, will require at least one permit.
147- return cost <= 0 ? 0 : Math .max (Math .min ((int ) (cost * 0x10000 ), maxPermits ), 1 );
148- }
149-
15042 /**
15143 * Constructs a {@code ConcurrentDownloadHandler} with the specified delegate.
15244 * The delegate is a
@@ -158,101 +50,6 @@ private static int costToPermits(float cost, int maxPermits) {
15850 this .delegate = delegate ;
15951 }
16052
161- /**
162- * Sets the timeout for acquiring a permit to start a download when there are
163- * not enough permits
164- * available in the semaphore.
165- *
166- * @see GridExporter#setConcurrentDownloadTimeout(long, TimeUnit)
167- * @return the timeout in nanoseconds.
168- */
169- public abstract long getTimeout ();
170-
171- /**
172- * Returns the cost of this download.
173- *
174- * Note that the method is not called under the session lock. It means that if
175- * implementation
176- * requires access to the application/session data then the session has to be
177- * locked explicitly.
178- *
179- * @param session vaadin session
180- * @see GridExporter#setConcurrentDownloadCost(float)
181- */
182- public float getCost (VaadinSession session ) {
183- return DEFAULT_COST ;
184- }
185-
186- /**
187- * Returns the UI associated with the current download.
188- * <p>
189- * This method is used to ensure that the UI is still attached to the current
190- * session when a
191- * download is initiated. Implementations should return the appropriate UI
192- * instance.
193- * </p>
194- *
195- * @return the {@link UI} instance associated with the current download, or
196- * {@code null} if no UI
197- * is available.
198- */
199- protected abstract UI getUI ();
200-
201- private UI getAttachedUI () {
202- return Optional .ofNullable (getUI ()).filter (UI ::isAttached ).orElse (null );
203- }
204-
205- /**
206- * Callback method that is invoked when a timeout occurs while trying to acquire
207- * a permit for
208- * starting a download.
209- * <p>
210- * Implementations can use this method to perform any necessary actions in
211- * response to the
212- * timeout, such as logging a warning or notifying the user.
213- * </p>
214- */
215- protected abstract void onTimeout ();
216-
217- /**
218- * Callback method that is invoked when a download is accepted.
219- * <p>
220- * This method is called at the start of the download process, right after the
221- * {@link #handleDownloadRequest(DownloadEvent) handleDownloadRequest} method is
222- * invoked and it
223- * has been determined that the download can proceed. Subclasses should
224- * implement this method to
225- * perform any necessary actions before the download begins, such as
226- * initializing resources,
227- * logging, or updating the UI to reflect the start of the download.
228- * <p>
229- * Note that this method is called before any semaphore permits are acquired, so
230- * it is executed
231- * regardless of whether the semaphore is enabled or not.
232- * </p>
233- */
234- protected abstract void onAccept ();
235-
236- /**
237- * Callback method that is invoked when a download finishes.
238- * <p>
239- * This method is called at the end of the download process, right before the
240- * {@link #handleDownloadRequest(DownloadEvent) handleDownloadRequest} method
241- * returns, regardless
242- * of whether the download was successful, timed out, or encountered an error.
243- * Subclasses should
244- * implement this method to perform any necessary actions after the download
245- * completes, such as
246- * releasing resources, logging, or updating the UI to reflect the completion of
247- * the download.
248- * <p>
249- * Note that this method is always called, even if an exception is thrown during
250- * the download
251- * process, ensuring that any necessary cleanup can be performed.
252- * </p>
253- */
254- protected abstract void onFinish ();
255-
25653 /**
25754 * Handles the download request using the provided {@link DownloadEvent}.
25855 * <p>
@@ -275,43 +72,7 @@ private UI getAttachedUI() {
27572 */
27673 @ Override
27774 public final void handleDownloadRequest (DownloadEvent event ) throws IOException {
278- onAccept ();
279- try {
280- if (!enabled ) {
281- delegate .handleDownloadRequest (event );
282- } else {
283-
284- try {
285- int permits ;
286- float cost = getCost (event .getSession ());
287- synchronized (semaphore ) {
288- permits = costToPermits (cost , semaphore .maxPermits );
289- }
290-
291- UI ui = failOnUiChange ? getAttachedUI () : null ;
292-
293- if (semaphore .tryAcquire (permits , getTimeout (), TimeUnit .NANOSECONDS )) {
294- try {
295- if (ui != null && getAttachedUI () != ui ) {
296- // The UI has changed or was detached after acquiring the semaphore
297- throw new IOException ("Detached UI" );
298- }
299- delegate .handleDownloadRequest (event );
300- } finally {
301- semaphore .release (permits );
302- }
303- } else {
304- onTimeout ();
305- throw new InterruptedByTimeoutException ();
306- }
307- } catch (InterruptedException e ) {
308- Thread .currentThread ().interrupt ();
309- throw (IOException ) new InterruptedIOException ().initCause (e );
310- }
311- }
312- } finally {
313- onFinish ();
314- }
75+ runWithSemaphore (event .getSession (), () -> delegate .handleDownloadRequest (event ));
31576 }
31677
31778}
0 commit comments