I'm using CSRFguard and it's working fine except for the Primefaces' fileupload case, where the token is not injected. All other filedownload, authentication, navigations, etc. work great. Any help would be appreciated!
My xhtml:
<p:commandButton value="#{telsaBean.downloadFile}" ajax="false" onclick="PrimeFaces.monitorDownload(start, stop)">
<p:fileDownload value="#{fileUploadControllerBean.downloadFile}" />
</p:commandButton>
My log:
*****************************************************
* Owasp.CsrfGuard Properties
*
* Logger: org.owasp.csrfguard.log.JavaLogger
* NewTokenLandingPage: null
* PRNG: SHA1PRNG
* SessionKey: OWASP_CSRFTOKEN
* TokenLength: 32
* TokenName: OWASP_CSRFTOKEN
* Ajax: true
* Rotate: false
* Javascript cache control: private, maxage=28800
* Javascript domain strict: true
* Javascript inject attributes: false
* Javascript inject forms: true
* Javascript referer pattern: .*
* Javascript referer match domain: true
* Javascript source file: WEB-INF/csrfguard.js
* Javascript X requested with: OWASP CSRFGuard Project
* Protected methods: Empty HashSet
* Protected pages size: 4
* Unprotected methods: HashSet size: 1: [0]: GET
* Unprotected pages size: 0
* TokenPerPage: true
* Enabled: true
* ValidateWhenNoSessionExists: false
* Action: org.owasp.csrfguard.action.Redirect
* Parameter: Page = /error.html
* Action: org.owasp.csrfguard.action.Log
* Parameter: Message = potential cross-site request forgery (CSRF) attack thwarted (user:%user%, ip:%remote_ip%, method:%request_method%, uri:%request_uri%, error:%exception_message%)
* Action: org.owasp.csrfguard.action.Rotate
*****************************************************
...
2022-05-04 11:13:57,459 WARNING [Owasp.CsrfGuard] (default task-225) potential cross-site request forgery (CSRF) attack thwarted (user:<anonymous>, ip:198.179.132.1, method:POST, uri:/index.jsf, error:required token is missing from the request)
Java:
public StreamedContent getDownloadFile(String sessionId, String username) {
final FileInputStream stream;
StreamedContent sfile = null;
logIt(sessionId, String.format("User %s downloads %s", username, fullName));
stream = new FileInputStream( fullName );
String contentType="text/plain";
sfile = DefaultStreamedContent.builder()
.contentType(contentType)
.name(name)
.stream(() -> stream).build();
return sfile;
}
Related
I have created a Hateoas enabled Rest service using spring-boot-starter-data-rest, works well.
I then created a client of that rest service in another spring boot module: this is a dependency that can be included in other projects that want to use the rest service. It uses a restTemplate under the hood.
It took a bit of mucking around with HttpMessageConverters and TypeConstrainedMappingJackson2HttpMessageConverter to get it to work but it does.
I tried using this dependency in my main application but it failed to populate the links in ResponseEntity< Resource< Myclass> >, leading to null pointer exceptions.
I couldn't track down the problem so I created a basic Spring Boot application 2.1.5.RELEASE and got the client working, then traced back the problem to this configuration in my main application which unfortunately is need for another problem:
spring:
main:
web-application-type: none
If this configuration is present it seems that hal+json isn't the first accepted media type
org.springframework.core.log.CompositeLog.debug(CompositeLog.java:147) : Accept=[application/json, application/hal+json, application/octet-stream, application/*+json]
When the configuration is removed I see
org.springframework.core.log.CompositeLog.debug(CompositeLog.java:147) : Accept=[application/hal+json, application/json, application/octet-stream, application/*+json]
and I can see this logged which fixes the issue I assume ( it isn't logged when the error happens)
- #ConditionalOnProperty (spring.hateoas.use-hal-as-default-json-media-type) matched (OnPropertyCondition)
I have tried adding this configuration to force the issue but it doesn't work
spring:
hateoas:
use-hal-as-default-json-media-type: true
This is my code in the rest client to configure the message converters:
#Configuration
public class MessageConverterConfiguration {
#Bean public TypeConstrainedMappingJackson2HttpMessageConverter myhalJacksonHttpMessageConverter(){
return new TypeConstrainedMappingJackson2HttpMessageConverter( ResourceSupport.class );
}
/**
* Add {#link TypeConstrainedMappingJackson2HttpMessageConverter} to the list of {#link HttpMessageConverter}s
* configured in the {#link RestTemplate} in first position ( this position is critical ).
* #param halJacksonHttpMessageConverter automagically configured by spring-boot-starter-hateoas
* #return List of {#link HttpMessageConverter}s
*/
#Bean( name = "hal-jackson" ) public List< HttpMessageConverter<?> > mymessageConverters( TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter ) {
final List<HttpMessageConverter<?>> all = new ArrayList<>( );
all.add( halJacksonHttpMessageConverter );
all.add( jacksonConverterWithOctetStreamSupport( ) );
all.addAll( new RestTemplate().getMessageConverters() );
return all;
}
/**
* This allows converting octet stream responses into {#link LastApplicationRun} ,
* when we create a last run by posting with {#link RestTemplate#postForObject(URI , Object, Class)}
* : without it we get a
* <pre>org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter
* found for response type [class com.sparknz.ced.spark.sampling.rest.tobesampled.client.domain.LastApplicationRun]
* and content type [application/octet-stream]</pre>.
* <p></p>
* I could find no better solution: it is not needed when we make a get call, don't understand why we get an octet stream response.
* It may only now be useful for tests.
*/
private MappingJackson2HttpMessageConverter jacksonConverterWithOctetStreamSupport( ) {
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
asList(new MediaType[]{
MediaType.valueOf( "application/hal+json" ) ,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_OCTET_STREAM }));
return converter;
}
}
What is 'web-application-type: none' doing and how can I get HypermediaHttpMessageConverterConfiguration to run?
I found that adding this to my configuration class did the trick:
#Import( RepositoryRestMvcConfiguration.class )
RepositoryRestMvcConfiguration seems to be responsible for making hal+json the highest priority by adding RepositoryRestMvcConfiguration.ResourceSupportHttpMessageConverter at position 0 in the list of HttpMessageConverters.
Problem:
I want to clear project history, when back button clicked by user on browser.
Technology using:
I am using php laravel-5.4.
Yes I got solution for my problem, I created middleware and used in route file, i got solution.
my middleware code is this,
namespace App\Http\Middleware;
use Closure;
class PreventBackHistory
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
return $response->header('Cache-Control','no-cache, no-store, max-age=0, must-revalidate')
->header('Pragma','no-cache')
->header('Expires','Sun, 02 Jan 1990 00:00:00 GMT');
}
}
I have this set up in security.yml;
failure_path: /login
This redirects a user to the log in page if they are not authenticated when trying to access specific URLs set in access_control e.g.
access_control:
- { path: ^/admin, role: ROLE_ADMIN }
But I cannot seem to capture this redirect.
I have tried to use a service for the security.authentication.failure event but this does not work
app.security.authentication_failure_event_listener:
class: MemberBundle\Event\AuthenticationListener
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
The redirect still occurs and the onAuthenticationFailure is never called. Presumably because authentication has not explicitly failed. The user just isn't logged in and is redirected.
The reason is because I want to give different response based on the expected format. For example I want html requests to go to the login page, but JSON requests should return valid JSON - and not the HTML login form page.
I feel like the FOSRestBundle may handle this, but it seems overkill for this relatively simple need. And its not a RESTFul web site so I shouldn't need that bundle to do this?
I also tried a service to listen for Exceptions but this did not work either. I guess the redirect that occurs doesn't throw an Exception?
app.exception_listener:
class: AppBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception }
So how can I always capture the redirect event that occurs when failure_path is set in security.yml?
My Tip (also for the future) is to "Create a Custom Authentication System with Guard. Much More simple and more flexible/customizable to handle that stuff (specially if a bit complicated with many authenticators like facebook, twitter, etc... and with a remember me option activated...).
But... as showed in the SecurityBundle Configuration ("security") you can set a failure_handler parameter for each login system in that firewall:
security:
firewalls:
your_firewall_name:
form_login:
failure_handler: your_custom_failure_handler_service_name <-- THIS ONE
Then you can create a service injecting the needed dependencies to handle the stuff you need and to return a different response based on the request format.
NOTE: I'm not sure if you can achieve this also creating a listener for the Security authentication event failure, but you can give it a try.
So the answer to this lay in a subtle thing about Symfony and the AccessDeniedException.
In that the Exception is only thrown based on your access controls if the current user is not allowed access. It is not thrown if you are not authenticated at all.
The same is true for any sort of authentication failure hook. Its only called with some sort of auth fails, not if you're simply not logged in and therefore not allowed.
So its done via two new services
app.exception_listener:
class: AppBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception }
app.entry_point:
class: AppBundle\EventListener\EntryPoint
arguments: ["#router"]
The first does capture the AccessDeniedException as you would expect when you're denied access to a resource.
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
do {
if ($exception instanceof AccessDeniedException) {
$request = $event->getRequest();
if ('json' == $request->get("format") || $request->isXmlHttpRequest()) {
$json = new JsonResponse(["status" => 0, 'not_logged_in' => true, "msg" => "You must be logged in"]);
$event->setResponse($json);
}
}
} while (null !== $exception = $exception->getPrevious());
}
}
The entry point service needs to be set in security.yml like to
security
firewalls:
main:
entry_point: app.entry_point
This class handles the start of the firewall and handles the redirection to authenticate. I check for the Exception is present and then test for XHR and json format and handle accordingly.
/**
* Returns a response that directs the user to authenticate.
*
* This is called when an anonymous request accesses a resource that
* requires authentication. The job of this method is to return some
* response that "helps" the user start into the authentication process.
*
* Examples:
* A) For a form login, you might redirect to the login page
* return new RedirectResponse('/login');
* B) For an API token authentication system, you return a 401 response
* return new Response('Auth header required', 401);
*
* #param Request $request The request that resulted in an AuthenticationException
* #param AuthenticationException $authException The exception that started the authentication process
*
* #return Response
*/
class EntryPoint implements AuthenticationEntryPointInterface
{
/** #var Router */
protected $router;
public function __construct(Router $router)
{
$this->router = $router;
}
public function start(Request $request, AuthenticationException $authException = null) {
if($authException) {
if('json' == $request->get("format") || $request->isXmlHttpRequest()) {
return new JsonResponse(["status" => 0, 'not_logged_in' => true, "msg" => "You must be logged in"]);
}
}
return new RedirectResponse($this->router->generate("fos_user_security_login"));
}
}
I am getting this error every time I try to post data to my server:
Server logs:
Starting the internal [HTTP/1.1] server on port 9192
Starting facilitymanager.api.rest.FacilityManagerAPIRestWrapper application
2015-06-22 13:18:11 127.0.0.1 - - 9192 POST /devices/rename - 415 554 45 64 http://localhost:9192 Java/1.7.0_79 -
Stopping the internal server
However In the service handler I am stating that I will handle JSON messages as you can see here:
public static final class RenameDevice extends ServerResource {
#Post("application/json")
public String doPost() throws InterruptedException, ConstraintViolationException, InvalidChoiceException, JSONException {
configureRestForm(this);
final String deviceId = getRequest().getAttributes().get("device_id").toString();
final String newName = getRequest().getAttributes().get("new_name").toString();
return renameDevice(deviceId, newName).toString(4);
}
}
/**
* Enables incoming connections from different servers.
*
* #param serverResource
* #return
*/
#SuppressWarnings({ "unchecked", "rawtypes" })
private static Series<Header> configureRestForm(ServerResource serverResource) {
Series<Header> responseHeaders = (Series<Header>) serverResource.getResponse().getAttributes()
.get("org.restlet.http.headers");
if (responseHeaders == null) {
responseHeaders = new Series(Header.class);
serverResource.getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
}
responseHeaders.add("Access-Control-Allow-Origin", "*");
responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
responseHeaders.add("Access-Control-Allow-Headers", "Content-Type");
responseHeaders.add("Access-Control-Allow-Credentials", "false");
responseHeaders.add("Access-Control-Max-Age", "60");
return responseHeaders;
}
What am I missing here?
Thanks!
Edit: This is the full log concerning the request:
Processing request to: "http://localhost:9192/devices/rename"
Call score for the "org.restlet.routing.VirtualHost#54594d1d" host: 1.0
Default virtual host selected
Base URI: "http://localhost:9192". Remaining part: "/devices/rename"
Call score for the "" URI pattern: 0.5
Selected route: "" -> facilitymanager.api.rest.FacilityManagerAPIRestWrapper#d75d3d7
Starting facilitymanager.api.rest.FacilityManagerAPIRestWrapper application
No characters were matched
Call score for the "/devices/list" URI pattern: 0.0
Call score for the "/groups/rename" URI pattern: 0.0
Call score for the "/devices/rename" URI pattern: 1.0
Selected route: "/devices/rename" -> Finder for RenameDevice
15 characters were matched
New base URI: "http://localhost:9192/devices/rename". No remaining part to match
Delegating the call to the target Restlet
Total score of variant "[text/html]"= 0.25
Total score of variant "[application/xhtml+xml]"= 5.0E-4
Converter selected for StatusInfo: StatusInfoHtmlConverter
2015-06-22 13:28:31 127.0.0.1 - - 9192 POST /devices/rename - 415 554 45 67 http://localhost:9192 Java/1.7.0_79 -
POST /devices/rename HTTP/1.1 [415 Unsupported Media Type] ()
KeepAlive stream used: http://localhost:9192/devices/rename
sun.net.www.MessageHeader#2bf4dee76 pairs: {null: HTTP/1.1 415 Unsupported Media Type}{Content-type: text/html; charset=UTF-8}{Content-length: 554}{Server: Restlet-Framework/3.0m1}{Accept-ranges: bytes}{Date: Mon, 22 Jun 2015 12:28:31 GMT}
To obtain a full log one must invoke this line of code anywhere before opening the restlet/component server:
// Create a new Component.
component = new Component();
// Add a new HTTP server listening on default port.
component.getServers().add(Protocol.HTTP, SERVER_PORT);
Engine.setLogLevel(Level.ALL); /// <----- HERE
component.start();
I've found the problem! The thing is that a tagged #Post method must receive an argument.
So the method should be like this:
#Post("application/json")
public String doPost(Representation entity) throws InterruptedException, ConstraintViolationException,
InvalidChoiceException, JSONException, IOException {
configureRestForm(this);
final Reader r = entity.getReader();
StringBuffer sb = new StringBuffer();
int c;
// Reads the JSON from the input stream
while ((c = r.read()) != -1) {
sb.append((char) c);
}
System.out.println(sb.toString()); // Shows the JSON received
}
}
The Representation entity argument brings you the means to detect the media type you are receiving. But since I have my tag like #Post("application/json") I do not need to verify this again.
Imagine that I use just "#Post" instead of "#Post("application/json")", I would have to validate the media type (or types) this way:
#Post
public Representation doPost(Representation entity)
throws ResourceException {
if (entity.getMediaType().isCompatible(MediaType.APPLICATION_JSON)) {
// ...
}
// ...
}
A method with an #Post annotation is not required to receive an argument, unless you intend to receive a payload from your request.
If you want to filter on the media type of the incoming representation, use the "json" shortcut, as follow
#Post("json")
This will prevent you to test the media type of the representation.
The list of all available shortcut is available here. Most of them are quite simple to remember. The main reason to use shortcuts (or "extension" such as file extension) is that "xml" is related to several media types (application/xml, text/xml).
If you want to get the full content of the representation, simply call the "getText()" method, instead of using the getReader() and consume it.
If you want to support CORS, I suggest you to use the CorsService (available in the 2.3 version of the Restlet Framework.
Notice there exists a shortcut for getting the headers from a Request or a Response, just call the "getHeaders()" method.
Notice there exists a shortcut for getting the attributes taken from the URL, just call the "getAttribute(String) method.
Here is an updated version of your source code:
public class TestApplication extends Application {
public final static class TestPostResource extends ServerResource {
#Post
public String doPost(Representation entity) throws Exception {
final String deviceId = getAttribute("device_id");
final String newName = getAttribute("new_name");
System.out.println(entity.getText());
System.out.println(getRequest().getHeaders());
System.out.println(getResponse().getHeaders());
return deviceId + "/" + newName;
}
}
public static void main(String[] args) throws Exception {
Component c = new Component();
c.getServers().add(Protocol.HTTP, 8183);
c.getDefaultHost().attach(new TestApplication());
CorsService corsService = new CorsService();
corsService.setAllowedOrigins(new HashSet<String>(Arrays.asList("*")));
corsService.setAllowedCredentials(true);
corsService.setSkippingResourceForCorsOptions(true);
c.getServices().add(corsService);
c.start();
}
#Override
public Restlet createInboundRoot() {
Router router = new Router(getContext());
router.attach("/testpost/{device_id}/{new_name}", TestPostResource.class);
return router;
}
}
I think everyone can agree that JFileChooser is really poop. So I was looking for an alternative and found out that JavaFX has got a great FileChooser class. So now the obvious question: how can I embed that neat FileChooser into my Swing application?
Needless to say, I did some research before I posted this, and this is what I found so far: link to a Reddit post.
The code of that JavaFXFileDialog class is very interesting, but it does not close when I exit my application (JavaFX seems to continue running in the background). Also I am missing some fields I can pass to the FileChooser like the path to set default directory. And I don't like how it is static.
I am grateful for any input.
The code of that dialog has multiple problems besides the ones you mention. For example, it doesn't handle the situation when JavaFX platform shuts down right after isJavaFXStillUsable() is called, but before the call to Platform.runLater(), which will still make it hang forever. I don't like that huge synchronized block either, although there don't seem to be any real problems with that. I also don't get why "the stupid synchronization object had to be a field" - each invocation of chooseFileWithJavaFXDialog() is independent of each other, so it could just as well use a local final lock (even that array would do fine).
The right way to make JVM exit correctly is to call Platform.exit() when you are shutting down your application (perhaps in windowClosed() of your main window). You need to do this manually because the chooser class has no idea whether you need JavaFX any more or not, and there is no way to restart it once it has been shut down.
That code inspired me to develop a utility class for calling just about any code in the JavaFX event thread, and get the result back to the calling thread, handling various exceptions and JavaFX states nicely:
/**
* A utility class to execute a Callable synchronously
* on the JavaFX event thread.
*
* #param <T> the return type of the callable
*/
public class SynchronousJFXCaller<T> {
private final Callable<T> callable;
/**
* Constructs a new caller that will execute the provided callable.
*
* The callable is accessed from the JavaFX event thread, so it should either
* be immutable or at least its state shouldn't be changed randomly while
* the call() method is in progress.
*
* #param callable the action to execute on the JFX event thread
*/
public SynchronousJFXCaller(Callable<T> callable) {
this.callable = callable;
}
/**
* Executes the Callable.
* <p>
* A specialized task is run using Platform.runLater(). The calling thread
* then waits first for the task to start, then for it to return a result.
* Any exception thrown by the Callable will be rethrown in the calling
* thread.
* </p>
* #param startTimeout time to wait for Platform.runLater() to <em>start</em>
* the dialog-showing task
* #param startTimeoutUnit the time unit of the startTimeout argument
* #return whatever the Callable returns
* #throws IllegalStateException if Platform.runLater() fails to start
* the task within the given timeout
* #throws InterruptedException if the calling (this) thread is interrupted
* while waiting for the task to start or to get its result (note that the
* task will still run anyway and its result will be ignored)
*/
public T call(long startTimeout, TimeUnit startTimeoutUnit)
throws Exception {
final CountDownLatch taskStarted = new CountDownLatch(1);
// Can't use volatile boolean here because only finals can be accessed
// from closures like the lambda expression below.
final AtomicBoolean taskCancelled = new AtomicBoolean(false);
// a trick to emulate modality:
final JDialog modalBlocker = new JDialog();
modalBlocker.setModal(true);
modalBlocker.setUndecorated(true);
modalBlocker.setOpacity(0.0f);
modalBlocker.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
final CountDownLatch modalityLatch = new CountDownLatch(1);
final FutureTask<T> task = new FutureTask<T>(() -> {
synchronized (taskStarted) {
if (taskCancelled.get()) {
return null;
} else {
taskStarted.countDown();
}
}
try {
return callable.call();
} finally {
// Wait until the Swing thread is blocked in setVisible():
modalityLatch.await();
// and unblock it:
SwingUtilities.invokeLater(() ->
modalBlocker.setVisible(false));
}
});
Platform.runLater(task);
if (!taskStarted.await(startTimeout, startTimeoutUnit)) {
synchronized (taskStarted) {
// the last chance, it could have been started just now
if (!taskStarted.await(0, TimeUnit.MILLISECONDS)) {
// Can't use task.cancel() here because it would
// interrupt the JavaFX thread, which we don't own.
taskCancelled.set(true);
throw new IllegalStateException("JavaFX was shut down"
+ " or is unresponsive");
}
}
}
// a trick to notify the task AFTER we have been blocked
// in setVisible()
SwingUtilities.invokeLater(() -> {
// notify that we are ready to get the result:
modalityLatch.countDown();
});
modalBlocker.setVisible(true); // blocks
modalBlocker.dispose(); // release resources
try {
return task.get();
} catch (ExecutionException ex) {
Throwable ec = ex.getCause();
if (ec instanceof Exception) {
throw (Exception) ec;
} else if (ec instanceof Error) {
throw (Error) ec;
} else {
throw new AssertionError("Unexpected exception type", ec);
}
}
}
}
The only part that worries me is that modality trick. It could very well work
without it (just remove any code that references modalBlocker and modalityHatch), but then the Swing part of the application won't just stop responding to the user input (which is what we need), but also will freeze,
stopping updates, progress bars and so on, which is not so nice. What worries me about this particular trick is that the invisible dialog may be not so invisible in some L&Fs, or cause other unwanted glitches.
I deliberately didn't include any initialization or shutdown code because I believe it doesn't belong there. I would just do new JFXPanel() in main() and Platform.exit() wherever I perform other shutdown tasks.
Using this class, calling for a FileChooser is easy:
/**
* A utility class that summons JavaFX FileChooser from the Swing EDT.
* (Or anywhere else for that matter.) JavaFX should be initialized prior to
* using this class (e. g. by creating a JFXPanel instance). It is also
* recommended to call Platform.setImplicitExit(false) after initialization
* to ensure that JavaFX platform keeps running. Don't forget to call
* Platform.exit() when shutting down the application, to ensure that
* the JavaFX threads don't prevent JVM exit.
*/
public class SynchronousJFXFileChooser {
private final Supplier<FileChooser> fileChooserFactory;
/**
* Constructs a new file chooser that will use the provided factory.
*
* The factory is accessed from the JavaFX event thread, so it should either
* be immutable or at least its state shouldn't be changed randomly while
* one of the dialog-showing method calls is in progress.
*
* The factory should create and set up the chooser, for example,
* by setting extension filters. If there is no need to perform custom
* initialization of the chooser, FileChooser::new could be passed as
* a factory.
*
* Alternatively, the method parameter supplied to the showDialog()
* function can be used to provide custom initialization.
*
* #param fileChooserFactory the function used to construct new choosers
*/
public SynchronousJFXFileChooser(Supplier<FileChooser> fileChooserFactory) {
this.fileChooserFactory = fileChooserFactory;
}
/**
* Shows the FileChooser dialog by calling the provided method.
*
* Waits for one second for the dialog-showing task to start in the JavaFX
* event thread, then throws an IllegalStateException if it didn't start.
*
* #see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* #param <T> the return type of the method, usually File or List<File>
* #param method a function calling one of the dialog-showing methods
* #return whatever the method returns
*/
public <T> T showDialog(Function<FileChooser, T> method) {
return showDialog(method, 1, TimeUnit.SECONDS);
}
/**
* Shows the FileChooser dialog by calling the provided method. The dialog
* is created by the factory supplied to the constructor, then it is shown
* by calling the provided method on it, then the result is returned.
* <p>
* Everything happens in the right threads thanks to
* {#link SynchronousJFXCaller}. The task performed in the JavaFX thread
* consists of two steps: construct a chooser using the provided factory
* and invoke the provided method on it. Any exception thrown during these
* steps will be rethrown in the calling thread, which shouldn't
* normally happen unless the factory throws an unchecked exception.
* </p>
* <p>
* If the calling thread is interrupted during either the wait for
* the task to start or for its result, then null is returned and
* the Thread interrupted status is set.
* </p>
* #param <T> return type (usually File or List<File>)
* #param method a function that calls the desired FileChooser method
* #param timeout time to wait for Platform.runLater() to <em>start</em>
* the dialog-showing task (once started, it is allowed to run as long
* as needed)
* #param unit the time unit of the timeout argument
* #return whatever the method returns
* #throws IllegalStateException if Platform.runLater() fails to start
* the dialog-showing task within the given timeout
*/
public <T> T showDialog(Function<FileChooser, T> method,
long timeout, TimeUnit unit) {
Callable<T> task = () -> {
FileChooser chooser = fileChooserFactory.get();
return method.apply(chooser);
};
SynchronousJFXCaller<T> caller = new SynchronousJFXCaller<>(task);
try {
return caller.call(timeout, unit);
} catch (RuntimeException | Error ex) {
throw ex;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return null;
} catch (Exception ex) {
throw new AssertionError("Got unexpected checked exception from"
+ " SynchronousJFXCaller.call()", ex);
}
}
/**
* Shows a FileChooser using FileChooser.showOpenDialog().
*
* #see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* #return the return value of FileChooser.showOpenDialog()
*/
public File showOpenDialog() {
return showDialog(chooser -> chooser.showOpenDialog(null));
}
/**
* Shows a FileChooser using FileChooser.showSaveDialog().
*
* #see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* #return the return value of FileChooser.showSaveDialog()
*/
public File showSaveDialog() {
return showDialog(chooser -> chooser.showSaveDialog(null));
}
/**
* Shows a FileChooser using FileChooser.showOpenMultipleDialog().
*
* #see #showDialog(java.util.function.Function, long, java.util.concurrent.TimeUnit)
* #return the return value of FileChooser.showOpenMultipleDialog()
*/
public List<File> showOpenMultipleDialog() {
return showDialog(chooser -> chooser.showOpenMultipleDialog(null));
}
public static void main(String[] args) {
javafx.embed.swing.JFXPanel dummy = new javafx.embed.swing.JFXPanel();
Platform.setImplicitExit(false);
try {
SynchronousJFXFileChooser chooser = new SynchronousJFXFileChooser(() -> {
FileChooser ch = new FileChooser();
ch.setTitle("Open any file you wish");
return ch;
});
File file = chooser.showOpenDialog();
System.out.println(file);
// this will throw an exception:
chooser.showDialog(ch -> ch.showOpenDialog(null), 1, TimeUnit.NANOSECONDS);
} finally {
Platform.exit();
}
}
}
Using this class, you may either initialize your chooser in the factory method, or, if you need to perform different initialization for each call, you could pass a custom method to showDialog() instead:
System.out.println(chooser.showDialog(ch -> {
ch.setInitialDirectory(new File(System.getProperty("user.home")));
return ch.showOpenDialog(null);
}));