1 //========================================================================
2 //$Id: HotDeployer.java 1113 2006-10-20 11:40:15Z janb $
3 //Copyright 2006 Mort Bay Consulting Pty. Ltd.
4 //------------------------------------------------------------------------
5 //Licensed under the Apache License, Version 2.0 (the "License");
6 //you may not use this file except in compliance with the License.
7 //You may obtain a copy of the License at
8 //http://www.apache.org/licenses/LICENSE-2.0
9 //Unless required by applicable law or agreed to in writing, software
10 //distributed under the License is distributed on an "AS IS" BASIS,
11 //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 //See the License for the specific language governing permissions and
13 //limitations under the License.
14 //========================================================================
15
16 package org.mortbay.jetty.deployer;
17
18 import java.io.File;
19 import java.io.FilenameFilter;
20 import java.io.IOException;
21 import java.util.HashMap;
22 import java.util.Map;
23
24 import org.mortbay.component.AbstractLifeCycle;
25 import org.mortbay.jetty.Server;
26 import org.mortbay.jetty.handler.ContextHandler;
27 import org.mortbay.jetty.handler.ContextHandlerCollection;
28 import org.mortbay.log.Log;
29 import org.mortbay.resource.Resource;
30 import org.mortbay.util.Scanner;
31 import org.mortbay.xml.XmlConfiguration;
32
33 /**
34 * Context Deployer
35 *
36 * This deployer scans a designated directory by
37 * {@link #setConfigurationDir(String)} for the appearance/disappearance or
38 * changes to xml configuration files. The scan is performed at startup and at
39 * an optional hot deployment frequency specified by
40 * {@link #setScanInterval(int)}. By default, the scanning is NOT recursive,
41 * but can be made so by {@link #setRecursive(boolean)}.
42 *
43 * Each configuration file is in {@link XmlConfiguration} format and represents
44 * the configuration of a instance of {@link ContextHandler} (or a subclass
45 * specified by the XML <code>Configure</code> element).
46 *
47 * The xml should configure the context and the instance is deployed to the
48 * {@link ContextHandlerCollection} specified by {@link #setContexts(Server)}.
49 *
50 * Similarly, when one of these existing files is removed, the corresponding
51 * context is undeployed; when one of these files is changed, the corresponding
52 * context is undeployed, the (changed) xml config file reapplied to it, and
53 * then (re)deployed.
54 *
55 * Note that the context itself is NOT copied into the hot deploy directory. The
56 * webapp directory or war file can exist anywhere. It is the xml config file
57 * that points to it's location and deploys it from there.
58 *
59 * It means, for example, that you can keep a "read-only" copy of your webapp
60 * somewhere, and apply different configurations to it simply by dropping
61 * different xml configuration files into the configuration directory.
62 *
63 * @org.apache.xbean.XBean element="hotDeployer" description="Creates a hot deployer
64 * to watch a directory for changes at a configurable interval."
65 *
66 */
67 public class ContextDeployer extends AbstractLifeCycle
68 {
69 public final static String NAME="ConfiguredDeployer";
70 private int _scanInterval=10;
71 private Scanner _scanner;
72 private ScannerListener _scannerListener;
73 private Resource _configurationDir;
74 private Map _currentDeployments=new HashMap();
75 private ContextHandlerCollection _contexts;
76 private ConfigurationManager _configMgr;
77 private boolean _recursive = false;
78
79 /* ------------------------------------------------------------ */
80 protected class ScannerListener implements Scanner.DiscreteListener
81 {
82 /**
83 * Handle a new deployment
84 *
85 * @see org.mortbay.util.Scanner.FileAddedListener#fileAdded(java.lang.String)
86 */
87 public void fileAdded(String filename) throws Exception
88 {
89 deploy(filename);
90 }
91
92 /**
93 * Handle a change to an existing deployment. Undeploy then redeploy.
94 *
95 * @see org.mortbay.util.Scanner.FileChangedListener#fileChanged(java.lang.String)
96 */
97 public void fileChanged(String filename) throws Exception
98 {
99 redeploy(filename);
100 }
101
102 /**
103 * Handle an undeploy.
104 *
105 * @see org.mortbay.util.Scanner.FileRemovedListener#fileRemoved(java.lang.String)
106 */
107 public void fileRemoved(String filename) throws Exception
108 {
109 undeploy(filename);
110 }
111 public String toString()
112 {
113 return "ContextDeployer$Scanner";
114 }
115 }
116
117 /**
118 * Constructor
119 *
120 * @throws Exception
121 */
122 public ContextDeployer() throws Exception
123 {
124 _scanner=new Scanner();
125 }
126
127 /* ------------------------------------------------------------ */
128 /**
129 * @return the ContextHandlerColletion to which to deploy the contexts
130 */
131 public ContextHandlerCollection getContexts()
132 {
133 return _contexts;
134 }
135
136 /* ------------------------------------------------------------ */
137 /**
138 * Associate with a {@link ContextHandlerCollection}.
139 *
140 * @param contexts
141 * the ContextHandlerColletion to which to deploy the contexts
142 */
143 public void setContexts(ContextHandlerCollection contexts)
144 {
145 if (isStarted()||isStarting())
146 throw new IllegalStateException("Cannot set Contexts after deployer start");
147 _contexts=contexts;
148 }
149
150 /* ------------------------------------------------------------ */
151 /**
152 * @param seconds
153 * The period in second between scans for changed configuration
154 * files. A zero or negative interval disables hot deployment
155 */
156 public void setScanInterval(int seconds)
157 {
158 if (isStarted()||isStarting())
159 throw new IllegalStateException("Cannot change scan interval after deployer start");
160 _scanInterval=seconds;
161 }
162
163 /* ------------------------------------------------------------ */
164 public int getScanInterval()
165 {
166 return _scanInterval;
167 }
168
169 /* ------------------------------------------------------------ */
170 /**
171 * @param dir
172 * @throws Exception
173 */
174 public void setConfigurationDir(String dir) throws Exception
175 {
176 setConfigurationDir(Resource.newResource(dir));
177 }
178
179 /* ------------------------------------------------------------ */
180 /**
181 * @param file
182 * @throws Exception
183 */
184 public void setConfigurationDir(File file) throws Exception
185 {
186 setConfigurationDir(Resource.newResource(file.toURL()));
187 }
188
189 /* ------------------------------------------------------------ */
190 /**
191 * @param resource
192 */
193 public void setConfigurationDir(Resource resource)
194 {
195 if (isStarted()||isStarting())
196 throw new IllegalStateException("Cannot change hot deploy dir after deployer start");
197 _configurationDir=resource;
198 }
199
200 /* ------------------------------------------------------------ */
201 /**
202 * @param directory
203 */
204 public void setDirectory(String directory) throws Exception
205 {
206 setConfigurationDir(directory);
207 }
208
209 /* ------------------------------------------------------------ */
210 /**
211 * @return
212 */
213 public String getDirectory()
214 {
215 return getConfigurationDir().getName();
216 }
217
218 /* ------------------------------------------------------------ */
219 /**
220 * @return
221 */
222 public Resource getConfigurationDir()
223 {
224 return _configurationDir;
225 }
226
227 /* ------------------------------------------------------------ */
228 /**
229 * @param configMgr
230 */
231 public void setConfigurationManager(ConfigurationManager configMgr)
232 {
233 _configMgr=configMgr;
234 }
235
236 /* ------------------------------------------------------------ */
237 /**
238 * @return
239 */
240 public ConfigurationManager getConfigurationManager()
241 {
242 return _configMgr;
243 }
244
245
246 public void setRecursive (boolean recursive)
247 {
248 _recursive=recursive;
249 }
250
251 public boolean getRecursive ()
252 {
253 return _recursive;
254 }
255
256 public boolean isRecursive()
257 {
258 return _recursive;
259 }
260 /* ------------------------------------------------------------ */
261 private void deploy(String filename) throws Exception
262 {
263 ContextHandler context=createContext(filename);
264 Log.info("Deploy "+filename+" -> "+ context);
265 _contexts.addHandler(context);
266 _currentDeployments.put(filename,context);
267 if (_contexts.isStarted())
268 context.start();
269 }
270
271 /* ------------------------------------------------------------ */
272 private void undeploy(String filename) throws Exception
273 {
274 ContextHandler context=(ContextHandler)_currentDeployments.get(filename);
275 Log.info("Undeploy "+filename+" -> "+context);
276 if (context==null)
277 return;
278 context.stop();
279 _contexts.removeHandler(context);
280 _currentDeployments.remove(filename);
281 }
282
283 /* ------------------------------------------------------------ */
284 private void redeploy(String filename) throws Exception
285 {
286 undeploy(filename);
287 deploy(filename);
288 }
289
290 /* ------------------------------------------------------------ */
291 /**
292 * Start the hot deployer looking for webapps to deploy/undeploy
293 *
294 * @see org.mortbay.component.AbstractLifeCycle#doStart()
295 */
296 protected void doStart() throws Exception
297 {
298 if (_configurationDir==null)
299 throw new IllegalStateException("No configuraition dir specified");
300
301 if (_contexts==null)
302 throw new IllegalStateException("No context handler collection specified for deployer");
303
304 _scanner.setScanDir(_configurationDir.getFile());
305 _scanner.setScanInterval(getScanInterval());
306 _scanner.setRecursive(_recursive); //only look in the top level for deployment files?
307 // Accept changes only in files that could be a deployment descriptor
308 _scanner.setFilenameFilter(new FilenameFilter()
309 {
310 public boolean accept(File dir, String name)
311 {
312 try
313 {
314 if (name.endsWith(".xml")&&dir.equals(getConfigurationDir().getFile()))
315 return true;
316 return false;
317 }
318 catch (IOException e)
319 {
320 Log.warn(e);
321 return false;
322 }
323 }
324 });
325 _scannerListener=new ScannerListener();
326 _scanner.addListener(_scannerListener);
327 _scanner.scan();
328 _scanner.start();
329 _contexts.getServer().getContainer().addBean(_scanner);
330 }
331
332 /* ------------------------------------------------------------ */
333 /**
334 * Stop the hot deployer.
335 *
336 * @see org.mortbay.component.AbstractLifeCycle#doStop()
337 */
338 protected void doStop() throws Exception
339 {
340 _scanner.removeListener(_scannerListener);
341 _scanner.stop();
342 }
343
344 /* ------------------------------------------------------------ */
345 /**
346 * Create a WebAppContext for the webapp being hot deployed, then apply the
347 * xml config file to it to configure it.
348 *
349 * @param filename
350 * the config file found in the hot deploy directory
351 * @return
352 * @throws Exception
353 */
354 private ContextHandler createContext(String filename) throws Exception
355 {
356 // The config file can call any method on WebAppContext to configure
357 // the webapp being deployed.
358 Resource resource = Resource.newResource(filename);
359 if (!resource.exists())
360 return null;
361
362 XmlConfiguration xmlConfiguration=new XmlConfiguration(resource.getURL());
363 HashMap properties = new HashMap();
364 properties.put("Server", _contexts.getServer());
365 if (_configMgr!=null)
366 properties.putAll(_configMgr.getProperties());
367
368 xmlConfiguration.setProperties(properties);
369 ContextHandler context=(ContextHandler)xmlConfiguration.configure();
370 return context;
371 }
372
373 }