001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.utils;
024
025import java.io.File;
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import dk.netarkivet.common.CommonSettings;
033import dk.netarkivet.common.Constants;
034import dk.netarkivet.common.exceptions.ArgumentNotValid;
035import dk.netarkivet.common.exceptions.PermissionDenied;
036import dk.netarkivet.common.lifecycle.LifeCycleComponent;
037import dk.netarkivet.common.management.MBeanConnectorCreator;
038
039/**
040 * This class provides functionality for starting applications.
041 */
042public abstract class ApplicationUtils {
043
044    /** logger for this class. */
045    private static final Logger log = LoggerFactory.getLogger(ApplicationUtils.class);
046
047    /** System.exit() value for the case where wrong arguments are given. */
048    public static final int WRONG_ARGUMENTS = 1;
049
050    /**
051     * System.exit() value for the case where the application does not have a factory method.
052     */
053    public static final int NO_FACTORY_METHOD = 2;
054
055    /**
056     * System.exit() value for the case where the application class could not be instantiated.
057     */
058    public static final int EXCEPTION_WHILE_INSTANTIATING = 3;
059
060    /**
061     * System.exit() value for the case where the Shutdown Hook for the application could not be added.
062     */
063    public static final int EXCEPTION_WHEN_ADDING_SHUTDOWN_HOOK = 4;
064
065    /**
066     * System.exit() value for the case where the management registration for the application could not be started.
067     */
068    public static final int EXCEPTION_WHEN_ADDING_MANAGEMENT = 6;
069
070    /**
071     * Helper class that prints a String to STDOUT, and logs it at INFO level at the same time.
072     *
073     * @param s the given string.
074     */
075    private static void logAndPrint(String s) {
076        System.out.println(s);
077        log.info(s);
078    }
079
080    /**
081     * Helper class for logging an exception (at level fatal) and printing it to STDOUT at the same time. Also invokes
082     * the error notifier.
083     *
084     * @param s a given String.
085     * @param t a given Exception.
086     */
087    private static void logExceptionAndPrint(String s, Throwable t) {
088        System.out.println(s);
089        t.printStackTrace();
090        log.error(s, t);
091        NotificationsFactory.getInstance().notify(s, NotificationType.ERROR, t);
092    }
093
094    /**
095     * Checks that the arguments for a class are empty. Exits the JVM with error code 1 if the arguments are not empty.
096     *
097     * @param args the argument array.
098     */
099    private static void checkArgs(String[] args) {
100        if (showVersion(args)) {
101            logAndPrint("NetarchiveSuite " + dk.netarkivet.common.Constants.getVersionString());
102            System.exit(0);
103        }
104        if (args.length > 0) {
105            logAndPrint("This application takes no arguments");
106            System.exit(WRONG_ARGUMENTS);
107        }
108    }
109
110    /**
111     * Should we show the version of NetarchiveSuite.
112     *
113     * @param args commandline arguments to NetarchiveSuite
114     * @return true, if we should show the version of NetarchiveSuite; otherwise false
115     */
116    private static boolean showVersion(String[] args) {
117        if (args.length == 1 && (args[0].equals("-v") || args[0].equals("--version"))) {
118            return true;
119        }
120        return false;
121    }
122
123    /**
124     * Starts up an application. The applications class must: (i) Have a static getInstance() method which returns a an
125     * instance of itself. (ii) Implement CleanupIF. If the class cannot be started and a shutdown hook added, the JVM
126     * exits with a return code depending on the problem: 1 means wrong arguments 2 means no factory method exists for
127     * class 3 means couldn't instantiate class 4 means couldn't add shutdown hook 5 means couldn't add liveness logger
128     * 6 means couldn't add remote management
129     *
130     * @param c The class to be started.
131     * @param args The arguments to the application (should be empty).
132     */
133    @SuppressWarnings({"rawtypes", "unchecked"})
134    public static void startApp(Class c, String[] args) {
135        String appName = c.getName();
136        Settings.set(CommonSettings.APPLICATION_NAME, appName);
137        logAndPrint("Starting " + appName + "\n" + Constants.getVersionString());
138        logAndPrint("Java VM: " + System.getProperty("java.version"));
139        logAndPrint("java.home: " + System.getProperty("java.home"));
140        logAndPrint("Working dir: " + System.getProperty("user.dir"));
141        log.info("Using settings files '{}'", StringUtils.conjoin(File.pathSeparator, Settings.getSettingsFiles()));
142        checkArgs(args);
143        dirMustExist(FileUtils.getTempDir());
144        Method factoryMethod = null;
145        CleanupIF instance = null;
146        // Start the remote management connector
147        try {
148            MBeanConnectorCreator.exposeJMXMBeanServer();
149            log.trace("Added remote management for {}", appName);
150        } catch (Throwable e) {
151            logExceptionAndPrint("Could not add remote management for class " + appName, e);
152            System.exit(EXCEPTION_WHEN_ADDING_MANAGEMENT);
153        }
154        // Get the factory method
155        try {
156            factoryMethod = c.getMethod("getInstance", (Class[]) null);
157            int modifier = factoryMethod.getModifiers();
158            if (!Modifier.isStatic(modifier)) {
159                throw new Exception("getInstance is not static");
160            }
161            logAndPrint(appName + " Running");
162        } catch (Throwable e) {
163            logExceptionAndPrint("Class " + appName + " does not have required factory method", e);
164            System.exit(NO_FACTORY_METHOD);
165        }
166        // Invoke the factory method
167        try {
168            log.trace("Invoking factory method.");
169            instance = (CleanupIF) factoryMethod.invoke(null, (Object[]) null);
170            log.trace("Factory method invoked.");
171        } catch (Throwable e) {
172            logExceptionAndPrint("Could not start class " + appName, e);
173            System.exit(EXCEPTION_WHILE_INSTANTIATING);
174        }
175        // Add the shutdown hook
176        try {
177            log.trace("Adding shutdown hook for " + appName);
178            Runtime.getRuntime().addShutdownHook((new CleanupHook(instance)));
179            log.trace("Added shutdown hook for " + appName);
180        } catch (Throwable e) {
181            logExceptionAndPrint("Could not add shutdown hook for class " + appName, e);
182            System.exit(EXCEPTION_WHEN_ADDING_SHUTDOWN_HOOK);
183        }
184    }
185
186    /**
187     * Starts up an LifeCycleComponent.
188     *
189     * @param component The component to start.
190     */
191    public static void startApp(LifeCycleComponent component) {
192        ArgumentNotValid.checkNotNull(component, "LifeCycleComponent component");
193
194        String appName = component.getClass().getName();
195        Settings.set(CommonSettings.APPLICATION_NAME, appName);
196        logAndPrint("Starting " + appName + "\n" + Constants.getVersionString());
197        log.info("Using settings files '{}'", StringUtils.conjoin(File.pathSeparator, Settings.getSettingsFiles()));
198        dirMustExist(FileUtils.getTempDir());
199        // Start the remote management connector
200        try {
201            MBeanConnectorCreator.exposeJMXMBeanServer();
202            log.trace("Added remote management for {}", appName);
203        } catch (Throwable e) {
204            logExceptionAndPrint("Could not add remote management for class " + appName, e);
205            System.exit(EXCEPTION_WHEN_ADDING_MANAGEMENT);
206        }
207
208        component.start();
209        logAndPrint(appName + " Running");
210
211        // Add the shutdown hook
212        try {
213            log.trace("Adding shutdown hook for {}", appName);
214            Runtime.getRuntime().addShutdownHook((new ShutdownHook(component)));
215            log.trace("Added shutdown hook for {}", appName);
216        } catch (Throwable e) {
217            logExceptionAndPrint("Could not add shutdown hook for class " + appName, e);
218            System.exit(EXCEPTION_WHEN_ADDING_SHUTDOWN_HOOK);
219        }
220    }
221
222    /**
223     * Ensure that a directory is available and writable. Will warn if the directory doesn't already exist (it ought to
224     * be created by the install script) and throws a PermissionDenied exception if the directory cannot be created.
225     *
226     * @param dir A File object denoting a directory.
227     * @throws PermissionDenied if the directory doesn't exist and cannot be created/written to, or if the File object
228     * indicates an existing non-directory.
229     */
230    public static void dirMustExist(File dir) {
231        ArgumentNotValid.checkNotNull(dir, "File dir");
232        if (FileUtils.createDir(dir)) {
233            log.warn("Non-existing directory created '{}'", dir);
234        }
235    }
236
237}