Ombrax
Ombrax

Reputation: 95

Java(FX) - Only allow 1 class to call a method from a singleton class

I'm currently working on a project where I'm using a singleton class to change the view for the user. This DisplayManager (the singleton) has methods like addView(View view) and replaceView(View view). But it also has a method called displayRootView() which may only be called once (during init) and by only 1 class, i.e. the StartUp class that extends the Application class.

Any idea as to how I can prevent other classes who use the singleton from calling the displayRootView() method?

I already considered StackTrace, but that doesn't seem to be ideal. I though maybe by using a tagging interface on the StartUp class to seperate it from the rest?

Any suggestions would be greatly appreciated.

Upvotes: 1

Views: 1276

Answers (4)

jewelsea
jewelsea

Reputation: 159331

The JavaFX Application source does some funky stuff by parsing stack traces to determine and check the caller (e.g. to ensure that it is only called from a class which extends Application).

/**
 * Launch a standalone application. This method is typically called
 * from the main method(). It must not be called more than once or an
 * exception will be thrown.
 * This is equivalent to launch(TheClass.class, args) where TheClass is the
 * immediately enclosing class of the method that called launch. It must
 * be a subclass of Application or a RuntimeException will be thrown.
 *
 * <p>
 * The launch method does not return until the application has exited,
 * either via a call to Platform.exit or all of the application windows
 * have been closed.
 *
 * <p>
 * Typical usage is:
 * <ul>
 * <pre>
 * public static void main(String[] args) {
 *     Application.launch(args);
 * }
 * </pre>
 * </ul>
 *
 * @param args the command line arguments passed to the application.
 *             An application may get these parameters using the
 *             {@link #getParameters()} method.
 *
 * @throws IllegalStateException if this method is called more than once.
 */
public static void launch(String... args) {
    // Figure out the right class to call
    StackTraceElement[] cause = Thread.currentThread().getStackTrace();

    boolean foundThisMethod = false;
    String callingClassName = null;
    for (StackTraceElement se : cause) {
        // Skip entries until we get to the entry for this class
        String className = se.getClassName();
        String methodName = se.getMethodName();
        if (foundThisMethod) {
            callingClassName = className;
            break;
        } else if (Application.class.getName().equals(className)
                && "launch".equals(methodName)) {

            foundThisMethod = true;
        }
    }

    if (callingClassName == null) {
        throw new RuntimeException("Error: unable to determine Application class");
    }

    try {
        Class theClass = Class.forName(callingClassName, true,
                           Thread.currentThread().getContextClassLoader());
        if (Application.class.isAssignableFrom(theClass)) {
            Class<? extends Application> appClass = theClass;
            LauncherImpl.launchApplication(appClass, args);
        } else {
            throw new RuntimeException("Error: " + theClass
                    + " is not a subclass of javafx.application.Application");
        }
    } catch (RuntimeException ex) {
        throw ex;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

The LauncherImpl code makes use of a getAndSet operation on a private static AtomicBoolean to ensure that the application is not launched more than once.

// Ensure that launchApplication method is only called once
private static AtomicBoolean launchCalled = new AtomicBoolean(false);

/**
 * This method is called by the standalone launcher.
 * It must not be called more than once or an exception will be thrown.
 *
 * Note that it is always called on a thread other than the FX application
 * thread, since that thread is only created at startup.
 *
 * @param appClass application class
 * @param preloaderClass preloader class, may be null
 * @param args command line arguments
 */
public static void launchApplication(final Class<? extends Application> appClass,
        final Class<? extends Preloader> preloaderClass,
        final String[] args) {

    if (launchCalled.getAndSet(true)) {
        throw new IllegalStateException("Application launch must not be called more than once");
    }

    if (! Application.class.isAssignableFrom(appClass)) {
        throw new IllegalArgumentException("Error: " + appClass.getName()
                + " is not a subclass of javafx.application.Application");
    }

    if (preloaderClass != null && ! Preloader.class.isAssignableFrom(preloaderClass)) {
        throw new IllegalArgumentException("Error: " + preloaderClass.getName()
                + " is not a subclass of javafx.application.Preloader");
    }

    // Create a new Launcher thread and then wait for that thread to finish
    final CountDownLatch launchLatch = new CountDownLatch(1);
    Thread launcherThread = new Thread(new Runnable() {
        @Override public void run() {
            try {
                launchApplication1(appClass, preloaderClass, args);
            } catch (RuntimeException rte) {
                launchException = rte;
            } catch (Exception ex) {
                launchException =
                    new RuntimeException("Application launch exception", ex);
            } catch (Error err) {
                launchException =
                    new RuntimeException("Application launch error", err);
            } finally {
                launchLatch.countDown();
            }
        }
    });
    launcherThread.setName("JavaFX-Launcher");
    launcherThread.start();

    // Wait for FX launcher thread to finish before returning to user
    try {
        launchLatch.await();
    } catch (InterruptedException ex) {
        throw new RuntimeException("Unexpected exception: ", ex);
    }

    if (launchException != null) {
        throw launchException;
    }
}

So it's kind of complicated and weird, but if you want a proven solution which works for the JavaFX code base, you could try to parse through it to understand what is going on and adapt it to your situation.

I'd say only introduce this additional complexity to your application if it is something vital for your application to have.

Orodous makes some excellent points of how obstructive such logic can be for unit testing. For example take a look at this advice on unit testing parts of a JavaFX application. Due to the weird checks in the launcher, to independently test functionality of their application, the developer needs to go through strange hoops to bypass the invoking any of the launcher code (e.g. initializing JavaFX using a Swing based JFXPanel instead of an a Application because an Application can only be launched once).

Upvotes: 2

VinceOPS
VinceOPS

Reputation: 2720

You could consider using the "Romeo and Juliet" trick given here, originally to simulate the "friend" mechanism (from C++) in Java.

Upvotes: 0

Ordous
Ordous

Reputation: 3884

Uh, this is difficult to prevent certain classes from calling your method, since it breaks some core OOP principles. The method shouldn't care who calls it. It's basic separation of concerns - your method should have a clear contract about what it does, not about what's the state of the JVM at the time.

Consider these questions:

  • What will happen if you subclass StartUp? For example to separate Desktop, Mobile and Web platforms?
  • How will you unit test this method without involving StartUp?
  • What happens if you need another layer of abstraction there?
  • What about if and when you add proxies (Via AOP or Spring proxies)?
  • What happens if you need to call the method from a Timer? It's still going to be called from StartUp class source (and be correct), but it won't appear in the stack trace.

And other such considerations.

Throwing some kind of exception (like IllegalStateException, or a custom one), in case the method is called a second time is absolutely valid IMHO.

This looks like you may want static checks on your code, not in-code or runtime checks. I don't think it would be terribly difficult to add a custom rule to Findbugz or PMD to find all direct invocations of a method and check the calling class (and fail the build if it's called from other places). But I don't think such a check is actually useful.

In the end, there are a lot more chances that you will need a legit usage of the method outside the said class, than there is that someone will accidentally use it incorrectly after they have been warned and appropriate Javadoc has been created.

Upvotes: 3

Christian Junker
Christian Junker

Reputation: 63

I would throw an IllegalStateException in displayRootView() when it is called more than once.

Upvotes: 0

Related Questions