/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package pdfdb.app;
import java.io.*;
import java.net.ServerSocket;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import javax.swing.*;
import pdfdb.data.db.*;
import pdfdb.gui.frames.*;
import pdfdb.indexing.fileservice.PollingFileChangeMonitor;
import pdfdb.settings.*;
/** The main application class, ensures single instance and hooks the
* frame up to the data events.
* @author ug22cmg */
public class Application
{
private static Application instance = null;
private PollingFileChangeMonitor poller = null;
private MainFrame frame = null;
private ExecutorService pool = null;
private StartupProtector startup = null;
private static final Object LOCK = new Object();
private static final String FIRST_START_MESSAGE =
"Before you can run the application for the very\n" +
"first time you must save some important settings\n" +
"such as the directory/drive to be searched. The \n" +
"preferences window will now open so you can do this.";
/** Intializes the application and displays the main frame.
* @param rootDirectory The directory to listen to. */
public Application(String directory) throws Exception
{
try
{
//System.setErr(null);
//System.setOut(null);
InitializerService starter = new InitializerService(directory);
initializeApplication();
showInterface();
ensureSingularInstance();
pool.execute(starter);
}
catch (ApplicationAlreadyRunningException le)
{
showLockError();
}
catch (Throwable e)
{
error(true);
}
}
/** Initializes the application instance and registers a shutdown hook */
private void initializeApplication()
{
Thread shutdownThread = new Thread(new ShutdownHook());
Runtime.getRuntime().addShutdownHook(shutdownThread);
instance = this;
pool = Executors.newCachedThreadPool();
}
/** Initializes the indexing service.
* @param directory The directory to start indexing at.
* @throws java.sql.SQLException If an error occurs. */
private void initializeServices(String directory) throws SQLException
{
poller = new PollingFileChangeMonitor(directory);
poller.start();
}
/** Shows an instance of the
* @throws java.sql.SQLException */
private void showInterface() throws SQLException
{
MainFrame f = MainFrame.getMainFrame();
if (f == null)
{
frame = new MainFrame(false);
}
else
{
frame = f;
}
}
/** Creates the lock file used to ensure single instance.
* @throws java.io.IOException If the file cannot be created. */
private void ensureSingularInstance() throws
ApplicationAlreadyRunningException
{
pool.execute(startup = new StartupProtector());
}
/** Shows the lock error. */
private void showLockError()
{
String err = "Only one instance of the program can be run at a time.";
JOptionPane.showMessageDialog(null, err);
}
/** Gets the poller in a thread safe way.
* @return The poller instance. */
private synchronized PollingFileChangeMonitor getPoller()
{
return poller;
}
/** Gets the current application instance, in a thread safe way.
* @return The instance of the current application or null. */
private static Application getInstance()
{
synchronized (LOCK)
{
return instance;
}
}
/** Adds an observer to the file change pipeline.
* @param o The observer. */
public static void addFileChangeObserver(Observer o)
{
Application app = getInstance();
if (o == null)
{
throw new IllegalArgumentException();
}
if (app != null && app.getPoller() != null)
{
app.getPoller().addObserver(o);
}
}
/** Removes the file observer.
* @param o The observer to remove. */
public static void removeFileChangeObserver(Observer o)
{
Application app = getInstance();
if (o == null)
{
throw new IllegalArgumentException();
}
if (app != null && app.getPoller() != null)
{
app.getPoller().removeObserver(o);
}
}
/** Displays the preferences dialog when no preferences file exists.
* @throws java.io.IOException If an error occurs while saving
* or loading. */
private static void requestSettings() throws IOException
{
PreferencesFrame dialog = null;
new File(".services").createNewFile();
dialog =
new PreferencesFrame(JFrame.getFrames()[0]);
dialog.setVisible(true);
if (dialog.getResult() != PreferencesFrame.APPROVED)
{
System.err.println("User failed to enter valid preferences.");
System.exit(0);
}
}
/** Main entry-point for the application. Program arguments are not
* used.
* @param args Unused program arguments. */
public static void main(String[] args) throws Exception
{
if (args.length != 0 && args.length != 1)
{
System.out.println("Program arguments invalid.");
}
else
{
UserSettingsManager manager = UserSettingsManager.getInstance();
String location = manager.get("SEARCH_LOCATION");
if (location == null || location.equals("") ||
!new File(location).exists())
{
// Make a dummy main frame, this is used as the dialog must
// have an owner, but we dont connect it to data yet.
MainFrame f = new MainFrame(false);
JOptionPane.showMessageDialog(null, FIRST_START_MESSAGE);
f.setVisible(true);
requestSettings();
if (manager.get("SERVICES_LOCATION").equals(".services"))
{
new File(".services").delete();
PluginSettingsManager pm = PluginSettingsManager.getInstance();
pm.save();
}
location = manager.get("SEARCH_LOCATION");
}
new Application(args.length == 0 ? location : args[0]);
}
}
/** Shows the error message. and exits.
* @param showMessage Whether to show the error message or not. */
private void error(boolean showMessage)
{
if (showMessage)
{
String err = "There has been a problem and this application " +
"will now exit.";
JOptionPane.showMessageDialog(MainFrame.getMainFrame(), err);
}
System.exit(1);
}
/** Protects the database from multiple instances. Creates a server socket
* and if unavailable, we know the application is already running. The
* idea for this code is from:
* http://www.rbgrn.net/blog/2008/05/java-single-application-instance.html
* However the code is substantially different from that of the tutorial. */
private class StartupProtector implements Runnable
{
private ServerSocket lock = null;
private boolean shutdown = false;
/** Initializes and throws an ApplicationAlreadyRunningException
* if the application is running.
* @throws pdfdb.app.ApplicationAlreadyRunningException if running. */
public StartupProtector() throws ApplicationAlreadyRunningException
{
try
{
lock = new ServerSocket(8275);
}
catch (IOException ioe)
{
throw new ApplicationAlreadyRunningException();
}
}
/** Halts the lock */
public void stop()
{
shutdown = true;
}
/** Runs the lock */
@Override
public void run()
{
while (!shutdown)
{
try
{
lock.accept();
}
catch (IOException ex)
{
}
}
}
}
/** Initializes the application */
private class InitializerService implements Runnable
{
private String directory;
/** Initializes the service.
* @param directory The directory to start searching at. */
public InitializerService(String directory)
{
this.directory = directory;
}
/** Runs the InitializerService. */
@Override
public void run()
{
try
{
MainFrame.getMainFrame().setVisible(true);
MainFrame.getMainFrame().setLoadingIsVisible(true);
FileProvider.initialize();
initializeServices(directory);
MainFrame.getMainFrame().connect();
MainFrame.getMainFrame().setLoadingIsVisible(false);
}
catch (SQLException ex)
{
error(false);
}
}
}
/** Shuts down the application's resources. */
private class ShutdownHook implements Runnable
{
/** Called when System.exit is called, shuts down database and other
* secondary resources, and ensures prompt closure of the indexing
* service. */
@Override
public void run()
{
int i = 0;
if (poller != null)
{
poller.shutdown();
while (poller.isActive() && i < 500)
{
i++;
}
}
if (pool != null)
{
pool.shutdown();
}
if (frame != null)
{
frame.shutdown();
}
DatabaseConnection.shutdown();
instance = null;
if (startup != null)
{
startup.stop();
}
}
}
}