Skip to content

Commit e608b7c

Browse files
committed
refactor: extract common logic in a separate class
Extract common semaphore logic into ConcurrentOperationBase for reuse in concurrent handlers.
1 parent d9b1b9f commit e608b7c

File tree

4 files changed

+369
-513
lines changed

4 files changed

+369
-513
lines changed

src/main/java/com/flowingcode/vaadin/addons/gridexporter/ConcurrentDownloadHandler.java

Lines changed: 2 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,12 @@
1919
*/
2020
package com.flowingcode.vaadin.addons.gridexporter;
2121

22-
import com.vaadin.flow.component.UI;
2322
import com.vaadin.flow.server.streams.DownloadHandler;
2423
import com.vaadin.flow.server.streams.DownloadEvent;
2524
import com.vaadin.flow.server.VaadinSession;
2625
import java.io.IOException;
2726
import java.io.InterruptedIOException;
28-
import java.io.OutputStream;
2927
import 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
@@ -41,112 +35,10 @@
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

Comments
 (0)