001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017
018package org.apache.commons.daemon.support;
019
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Objects;
025
026import org.apache.commons.daemon.Daemon;
027import org.apache.commons.daemon.DaemonContext;
028
029/**
030 * Implementation of the Daemon that allows running
031 * standard applications as daemons.
032 * The applications must have the mechanism to manage
033 * the application lifecycle.
034 */
035public class DaemonWrapper implements Daemon
036{
037
038    private final static String ARGS            = "args";
039    private final static String START_CLASS     = "start";
040    private final static String START_METHOD    = "start.method";
041    private final static String STOP_CLASS      = "stop";
042    private final static String STOP_METHOD     = "stop.method";
043    private final static String STOP_ARGS       = "stop.args";
044    private String              configFileName;
045    private final DaemonConfiguration config;
046
047    private final Invoker             startup;
048    private final Invoker             shutdown;
049
050    /**
051     * Constructs a new initialized instance.
052     */
053    public DaemonWrapper()
054    {
055        config   = new DaemonConfiguration();
056        startup  = new Invoker();
057        shutdown = new Invoker();
058    }
059
060    /**
061     * Called from DaemonLoader on init stage.
062     * <p>
063     * Accepts the following configuration arguments:
064     * <ul>
065     * <li>-daemon-properties: - load DaemonConfiguration properties from the specified file to act as defaults</li>
066     * <li>-start: set start class name</li>
067     * <li>-start-method: set start method name</li>
068     * <li>-stop: set stop class name</li>
069     * <li>-stop-method: set stop method name</li>
070     * <li>-stop-argument: set optional argument to stop method</li>
071     * <li>Anything else is treated as a startup argument</li>
072     * </ul>
073     * <p>
074     * The following "-daemon-properties" are recognized:
075     * <ul>
076     * <li>args (startup argument)</li>
077     * <li>start</li>
078     * <li>start.method</li>
079     * <li>stop</li>
080     * <li>stop.method</li>
081     * <li>stop.args</li>
082     * </ul>
083     * These are used to set the corresponding item if it has not already been
084     * set by the command arguments. <b>However, note that args and stop.args are
085     * appended to any existing values.</b>
086     */
087    @Override
088    public void init(final DaemonContext context)
089        throws Exception
090    {
091        final String[] args = context.getArguments();
092
093        if (args != null) {
094            int i;
095            // Parse our arguments and remove them
096            // from the final argument array we are
097            // passing to our child.
098            arguments:
099            for (i = 0; i < args.length; i++) {
100                if (args[i].equals("--")) {
101                    // Done with argument processing
102                    break;
103                }
104                switch (args[i]) {
105                    case "-daemon-properties":
106                        if (++i == args.length) {
107                            throw new IllegalArgumentException(args[i - 1]);
108                        }
109                        configFileName = args[i];
110                        break;
111                    case "-start":
112                        if (++i == args.length) {
113                            throw new IllegalArgumentException(args[i - 1]);
114                        }
115                        startup.setClassName(args[i]);
116                        break;
117                    case "-start-method":
118                        if (++i == args.length) {
119                            throw new IllegalArgumentException(args[i - 1]);
120                        }
121                        startup.setMethodName(args[i]);
122                        break;
123                    case "-stop":
124                        if (++i == args.length) {
125                            throw new IllegalArgumentException(args[i - 1]);
126                        }
127                        shutdown.setClassName(args[i]);
128                        break;
129                    case "-stop-method":
130                        if (++i == args.length) {
131                            throw new IllegalArgumentException(args[i - 1]);
132                        }
133                        shutdown.setMethodName(args[i]);
134                        break;
135                    case "-stop-argument":
136                        if (++i == args.length) {
137                            throw new IllegalArgumentException(args[i - 1]);
138                        }
139                        final String[] aa = new String[1];
140                        aa[0] = args[i];
141                        shutdown.addArguments(aa);
142                        break;
143                    default:
144                        // This is not our option.
145                        // Everything else will be forwarded to the main
146                        break arguments;
147                }
148            }
149            if (args.length > i) {
150                final String[] copy = new String[args.length - i];
151                System.arraycopy(args, i, copy, 0, copy.length);
152                startup.addArguments(copy);
153            }
154        }
155        if (config.load(configFileName)) {
156            // Setup params if not set via cmdline.
157            startup.setClassName(config.getProperty(START_CLASS));
158            startup.setMethodName(config.getProperty(START_METHOD));
159            // Merge the config with command line arguments
160            startup.addArguments(config.getPropertyArray(ARGS));
161
162            shutdown.setClassName(config.getProperty(STOP_CLASS));
163            shutdown.setMethodName(config.getProperty(STOP_METHOD));
164            shutdown.addArguments(config.getPropertyArray(STOP_ARGS));
165        }
166        startup.validate();
167        shutdown.validate();
168    }
169
170    /**
171     */
172    @Override
173    public void start()
174        throws Exception
175    {
176        startup.invoke();
177    }
178
179    /**
180     */
181    @Override
182    public void stop()
183        throws Exception
184    {
185        shutdown.invoke();
186    }
187
188    /**
189     */
190    @Override
191    public void destroy()
192    {
193        // Nothing for the moment
194        System.err.println("DaemonWrapper: instance " + hashCode() + " destroy");
195    }
196
197    // Internal class for wrapping the start/stop methods
198    static class Invoker
199    {
200        private String      name;
201        private String      call;
202        private String[]    args;
203        private Method      inst;
204        private Class<?>    main;
205
206        protected Invoker()
207        {
208        }
209
210        protected void setClassName(final String name)
211        {
212            if (this.name == null) {
213                this.name = name;
214            }
215        }
216        protected void setMethodName(final String name)
217        {
218            if (this.call == null) {
219                this.call = name;
220            }
221        }
222        protected void addArguments(final String[] args)
223        {
224            if (args != null) {
225                final ArrayList<String> aa = new ArrayList<>();
226                if (this.args != null) {
227                    aa.addAll(Arrays.asList(this.args));
228                }
229                aa.addAll(Arrays.asList(args));
230                this.args = aa.toArray(DaemonConfiguration.EMPTY_STRING_ARRAY);
231            }
232        }
233
234        protected void invoke()
235            throws Exception
236        {
237            if (name.equals("System") && call.equals("exit")) {
238                // Just call a System.exit()
239                // The start method was probably installed
240                // a shutdown hook.
241                System.exit(0);
242            }
243            else {
244                Object obj   = null;
245                if ((inst.getModifiers() & Modifier.STATIC) == 0) {
246                    // We only need object instance for non-static methods.
247                    obj = main.getConstructor().newInstance();
248                }
249                final Object[] arg = new Object[1];
250
251                arg[0] = args;
252                inst.invoke(obj, arg);
253            }
254        }
255        // Load the class using reflection
256        protected void validate()
257            throws Exception
258        {
259            /* Check the class name */
260            if (name == null) {
261                name = "System";
262                call = "exit";
263                return;
264            }
265            if (args == null) {
266                args = new String[0];
267            }
268            if (call == null) {
269                call = "main";
270            }
271
272            // Get the ClassLoader loading this class
273            final ClassLoader classLoader = DaemonWrapper.class.getClassLoader();
274            Objects.requireNonNull(classLoader, "classLoader");
275            final Class<?>[] ca = new Class[1];
276            ca[0] = args.getClass();
277            // Find the required class
278            main = classLoader.loadClass(name);
279            if (main == null) {
280                throw new ClassNotFoundException(name);
281            }
282            // Find the required method.
283            // NoSuchMethodException will be thrown if matching method
284            // is not found.
285            inst = main.getMethod(call, ca);
286        }
287    }
288}