View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.transport.servlet.jetty;
8   
9   import org.mule.api.MuleContext;
10  import org.mule.api.MuleException;
11  import org.mule.api.MuleRuntimeException;
12  import org.mule.api.config.MuleProperties;
13  import org.mule.api.construct.FlowConstruct;
14  import org.mule.api.context.notification.MuleContextNotificationListener;
15  import org.mule.api.endpoint.ImmutableEndpoint;
16  import org.mule.api.endpoint.InboundEndpoint;
17  import org.mule.api.lifecycle.InitialisationException;
18  import org.mule.api.lifecycle.LifecycleException;
19  import org.mule.api.transport.MessageReceiver;
20  import org.mule.api.transport.ReplyToHandler;
21  import org.mule.config.i18n.CoreMessages;
22  import org.mule.context.notification.MuleContextNotification;
23  import org.mule.context.notification.NotificationException;
24  import org.mule.transport.AbstractConnector;
25  import org.mule.transport.servlet.JarResourceServlet;
26  import org.mule.transport.servlet.MuleReceiverServlet;
27  import org.mule.transport.servlet.MuleServletContextListener;
28  import org.mule.util.ClassUtils;
29  import org.mule.util.IOUtils;
30  import org.mule.util.StringMessageUtils;
31  import org.mule.util.StringUtils;
32  
33  import java.io.File;
34  import java.io.InputStream;
35  import java.net.URL;
36  import java.util.ArrayList;
37  import java.util.HashMap;
38  import java.util.List;
39  
40  import javax.servlet.Servlet;
41  import javax.servlet.http.HttpServlet;
42  
43  import org.mortbay.jetty.Connector;
44  import org.mortbay.jetty.Handler;
45  import org.mortbay.jetty.Server;
46  import org.mortbay.jetty.annotations.Configuration;
47  import org.mortbay.jetty.handler.ContextHandlerCollection;
48  import org.mortbay.jetty.nio.SelectChannelConnector;
49  import org.mortbay.jetty.servlet.Context;
50  import org.mortbay.jetty.servlet.ServletHolder;
51  import org.mortbay.jetty.webapp.WebAppContext;
52  import org.mortbay.jetty.webapp.WebInfConfiguration;
53  import org.mortbay.log.Log;
54  import org.mortbay.xml.XmlConfiguration;
55  
56  /**
57   * The <code>JettyConnector</code> can be using to embed a Jetty server to receive requests on an
58   * http inound endpoint. One server is created for each connector declared, many Jetty endpoints
59   * can share the same connector.
60   */
61  public class JettyHttpConnector extends AbstractConnector
62  {
63      public static final String ROOT = "/";
64  
65      public static final String JETTY = "jetty";
66  
67      private Server httpServer;
68  
69      private String configFile;
70  
71      private JettyReceiverServlet receiverServlet;
72  
73      private boolean useContinuations = false;
74  
75      private String resourceBase;
76  
77      private WebappsConfiguration webappsConfiguration;
78  
79      protected HashMap<String, ConnectorHolder> holders = new HashMap<String, ConnectorHolder>();
80  
81      private WebAppDeployer deployer;
82  
83      public JettyHttpConnector(MuleContext context)
84      {
85          super(context);
86          setupJettyLogging();
87          registerSupportedProtocol("http");
88          registerSupportedProtocol(JETTY);
89          setInitialStateStopped(true);
90      }
91  
92      protected void setupJettyLogging()
93      {
94          if ((Log.getLog() instanceof JettyLogger) == false)
95          {
96              Log.setLog(new JettyLogger());
97          }
98      }
99  
100     public String getProtocol()
101     {
102         return JETTY;
103     }
104 
105     @Override
106     protected void doInitialise() throws InitialisationException
107     {
108         httpServer = new Server()
109         {
110             @Override
111             public void addHandler(Handler handler)
112             {
113                 final Connector c = getServer().getConnectors()[0];
114                 if (handler instanceof WebAppContext)
115                 {
116                     final WebAppContext webapp = (WebAppContext) handler;
117                     final String msg = String.format("Will deploy a web app at %s:/%s%s%s",
118                                                      "http", c.getHost(),
119                                                      c.getPort() == 80 ? StringUtils.EMPTY : ":" + c.getPort(),
120                                                      webapp.getContextPath());
121 
122                     final File workDir = new File(muleContext.getConfiguration().getWorkingDirectory(),
123                                                   "_exploded_wars" + webapp.getContextPath());
124                     workDir.mkdirs();
125                     webapp.setTempDirectory(workDir);
126                     // TODO extract to a better constant
127                     webapp.setAttribute("muleContext", muleContext);
128 
129                     if (logger.isInfoEnabled())
130                     {
131                         logger.info(StringMessageUtils.getBoilerPlate(msg, '*', 70));
132                     }
133                 }
134                 super.addHandler(handler);
135             }
136         };
137         
138         if (webappsConfiguration != null)
139         {
140             deployer = new WebAppDeployer();
141             String webAppDir = webappsConfiguration.getDirectory();
142             if (StringUtils.isBlank(webAppDir))
143             {
144                 // if none specified, resolve defaults dynamically
145                 final String appDir = muleContext.getRegistry().get(MuleProperties.APP_HOME_DIRECTORY_PROPERTY);
146                 webAppDir = appDir + "/webapps";
147             }
148 
149             if (configFile == null)
150             {
151                 // override only if user hasn't specified one (turn off file-mapped buffer for
152                 // static files to avoid resource locking, makes webapp resources editable on the fly)
153                 final URL muleDefaults = ClassUtils.getResource("org/mule/transport/jetty/webdefault.xml", getClass());
154                 deployer.setDefaultsDescriptor(muleDefaults.toExternalForm());
155             }
156             deployer.setWebAppDir(webAppDir);
157             deployer.setExtract(true);
158             deployer.setParentLoaderPriority(false);
159             deployer.setServerClasses(webappsConfiguration.getServerClasses());
160             deployer.setSystemClasses(webappsConfiguration.getSystemClasses());
161 
162             org.mortbay.jetty.AbstractConnector jettyConnector = createJettyConnector();
163             jettyConnector.setHost(webappsConfiguration.getHost());
164             jettyConnector.setPort(webappsConfiguration.getPort());
165             deployer.setContexts(httpServer);
166             String[] confClasses = new String[]
167             {
168                 // configures webapp's classloader as a child of a Mule app classloader
169                 WebInfConfiguration.class.getName(),
170                 // just to get jetty going, we don't need java ee bindings. inherits annotation processing
171                 DummyJndiConfiguration.class.getName()
172             };
173             deployer.setConfigurationClasses(confClasses);
174 
175             httpServer.addConnector(jettyConnector);
176             httpServer.addLifeCycle(deployer);
177         }
178         
179         initialiseFromConfigFile();
180 
181         try
182         {
183             muleContext.registerListener(new MuleContextNotificationListener<MuleContextNotification>(){
184                 public void onNotification(MuleContextNotification notification)
185                 {
186                     if (notification.getAction() == MuleContextNotification.CONTEXT_STARTED)
187                     {
188                         //We delay starting until the context has been started since we need the MuleAjaxServlet to initialise first
189                         setInitialStateStopped(false);
190                         try
191                         {
192                             start();
193                             // update the agent displaying webapp urls to the user
194                             final JettyWebappServerAgent agent = (JettyWebappServerAgent) muleContext.getRegistry().lookupAgent(JettyWebappServerAgent.NAME);
195                             if (agent != null)
196                             {
197                                 agent.onJettyConnectorStarted(JettyHttpConnector.this);
198                             }
199                         }
200                         catch (MuleException e)
201                         {
202                             throw new MuleRuntimeException(CoreMessages.failedToStart(getName()), e);
203                         }
204                     }
205                 }
206             });
207         }
208         catch (NotificationException e)
209         {
210             throw new InitialisationException(e, this);
211         }
212     }
213 
214     @SuppressWarnings("unchecked")
215     protected void initialiseFromConfigFile() throws InitialisationException
216     {
217         if (configFile == null)
218         {
219             return;
220         }
221         try
222         {
223             InputStream is = IOUtils.getResourceAsStream(configFile, getClass());
224             XmlConfiguration config = new XmlConfiguration(is);
225 
226             String appHome =
227                 muleContext.getRegistry().lookupObject(MuleProperties.APP_HOME_DIRECTORY_PROPERTY);
228             if (appHome == null)
229             {
230                 // Mule IDE sets app.home as part of the launch config it creates
231                 appHome = System.getProperty(MuleProperties.APP_HOME_DIRECTORY_PROPERTY);
232             }
233 
234             if (appHome != null)
235             {
236                 config.getProperties().put(MuleProperties.APP_HOME_DIRECTORY_PROPERTY, appHome);
237             }
238 
239             config.configure(httpServer);
240         }
241         catch (Exception e)
242         {
243             throw new InitialisationException(e, this);
244         }
245     }
246 
247     /**
248      * Template method to dispose any resources associated with this receiver. There
249      * is not need to dispose the connector as this is already done by the framework
250      */
251     @Override
252     protected void doDispose()
253     {
254         holders.clear();
255     }
256 
257     @Override
258     protected void doStart() throws MuleException
259     {
260         try
261         {
262             httpServer.start();
263             
264             if (deployer != null)
265             {
266                 deployer.start();
267             }
268             
269             for (ConnectorHolder<?, ?> contextHolder : holders.values())
270             {
271                 contextHolder.start();
272             }
273         }
274         catch (Exception e)
275         {
276             throw new LifecycleException(CoreMessages.failedToStart("Jetty Http Receiver"), e, this);
277         }
278     }
279 
280     @Override
281     protected void doStop() throws MuleException
282     {
283         try
284         {
285             httpServer.stop();
286             
287             if (deployer != null)
288             {
289                 deployer.stop();
290             }
291             
292             for (ConnectorHolder<?, ?> connectorRef : holders.values())
293             {
294                 connectorRef.stop();
295             }
296         }
297         catch (Exception e)
298         {
299             throw new LifecycleException(CoreMessages.failedToStop("Jetty Http Receiver"), e, this);
300         }
301     }
302 
303     /**
304      * Template method where any connections should be made for the connector
305      *
306      * @throws Exception
307      */
308     @Override
309     protected void doConnect() throws Exception
310     {
311         //do nothing
312     }
313 
314     /**
315      * Template method where any connected resources used by the connector should be
316      * disconnected
317      *
318      * @throws Exception
319      */
320     @Override
321     protected void doDisconnect() throws Exception
322     {
323         //do nothing
324     }
325 
326      @Override
327     protected MessageReceiver createReceiver(FlowConstruct flowConstruct, InboundEndpoint endpoint) throws Exception
328     {
329         MessageReceiver receiver = super.createReceiver(flowConstruct, endpoint);
330         registerJettyEndpoint(receiver, endpoint);
331         return receiver;
332     }
333 
334     protected org.mortbay.jetty.AbstractConnector createJettyConnector()
335     {
336         return new SelectChannelConnector();
337     }
338 
339     public void unregisterListener(MessageReceiver receiver) throws MuleException
340     {
341         String connectorKey = getHolderKey(receiver.getEndpoint());
342 
343         synchronized (this)
344         {
345             ConnectorHolder connectorRef = holders.get(connectorKey);
346             if (connectorRef != null)
347             {
348                 if (!connectorRef.isReferenced())
349                 {
350                     getHttpServer().removeConnector(connectorRef.getConnector());
351                     holders.remove(connectorKey);
352                     connectorRef.stop();
353                 }
354             }
355         }
356     }
357 
358     public Server getHttpServer()
359     {
360         return httpServer;
361     }
362 
363     public String getConfigFile()
364     {
365         return configFile;
366     }
367 
368     public void setConfigFile(String configFile)
369     {
370         this.configFile = configFile;
371     }
372 
373     public JettyReceiverServlet getReceiverServlet()
374     {
375         return receiverServlet;
376     }
377 
378     public void setReceiverServlet(JettyReceiverServlet receiverServlet)
379     {
380         this.receiverServlet = receiverServlet;
381     }
382 
383     @Override
384     public ReplyToHandler getReplyToHandler(ImmutableEndpoint endpoint)
385     {
386         if (isUseContinuations())
387         {
388             return new JettyContinuationsReplyToHandler(getDefaultResponseTransformers(endpoint), muleContext);
389         }
390         return super.getReplyToHandler(endpoint);
391     }
392 
393     public boolean isUseContinuations()
394     {
395         return useContinuations;
396     }
397 
398     public void setUseContinuations(boolean useContinuations)
399     {
400         this.useContinuations = useContinuations;
401     }
402 
403     ConnectorHolder<? extends MuleReceiverServlet, ? extends JettyHttpMessageReceiver> registerJettyEndpoint(MessageReceiver receiver, InboundEndpoint endpoint) throws MuleException
404     {
405         // Make sure that there is a connector for the requested endpoint.
406         String connectorKey = getHolderKey(endpoint);
407 
408         ConnectorHolder holder;
409 
410         synchronized (this)
411         {
412             holder = holders.get(connectorKey);
413             if (holder == null)
414             {
415                 Connector connector = createJettyConnector();
416 
417                 connector.setPort(endpoint.getEndpointURI().getPort());
418                 connector.setHost(endpoint.getEndpointURI().getHost());
419                 if ("localhost".equalsIgnoreCase(endpoint.getEndpointURI().getHost()))
420                 {
421                     logger.warn("You use localhost interface! It means that no external connections will be available."
422                             + " Don't you want to use 0.0.0.0 instead (all network interfaces)?");
423                 }
424                 getHttpServer().addConnector(connector);
425 
426                 holder = createContextHolder(connector, receiver.getEndpoint(), receiver);
427                 holders.put(connectorKey, holder);
428                 if(isStarted())
429                 {
430                     holder.start();
431                 }
432             }
433             else
434             {
435                 holder.addReceiver(receiver);
436             }
437         }
438         return holder;
439     }
440 
441     protected ConnectorHolder createContextHolder(Connector connector, InboundEndpoint endpoint, MessageReceiver receiver)
442     {
443         return new MuleReceiverConnectorHolder(connector, (JettyReceiverServlet) createServlet(connector, endpoint), (JettyHttpMessageReceiver)receiver);
444     }
445 
446     protected Servlet createServlet(Connector connector, ImmutableEndpoint endpoint)
447     {
448         HttpServlet servlet;
449         if (getReceiverServlet() == null)
450         {
451             if(isUseContinuations())
452             {
453                 servlet = new JettyContinuationsReceiverServlet();
454             }
455             else
456             {
457                 servlet = new JettyReceiverServlet();
458             }
459         }
460         else
461         {
462             servlet = getReceiverServlet();
463         }
464 
465         String path = endpoint.getEndpointURI().getPath();
466         if(StringUtils.isBlank(path))
467         {
468             path = ROOT;
469         }
470 
471         ContextHandlerCollection handlerCollection = new ContextHandlerCollection();
472         Context context = new Context(handlerCollection, ROOT, Context.NO_SECURITY);
473         context.setConnectorNames(new String[]{connector.getName()});
474         context.addEventListener(new MuleServletContextListener(muleContext, getName()));
475 
476         if (resourceBase != null)
477         {
478             Context resourceContext = new Context(handlerCollection, path, Context.NO_SECURITY);
479             resourceContext.setResourceBase(resourceBase);
480         }
481 
482         context.addServlet(JarResourceServlet.class, JarResourceServlet.DEFAULT_PATH_SPEC);
483 
484         ServletHolder holder = new ServletHolder();
485         holder.setServlet(servlet);
486         context.addServlet(holder, "/*");
487         getHttpServer().addHandler(handlerCollection);
488         return servlet;
489     }
490 
491     protected String getHolderKey(ImmutableEndpoint endpoint)
492     {
493         return endpoint.getProtocol() + ":" + endpoint.getEndpointURI().getHost() + ":" + endpoint.getEndpointURI().getPort();
494     }
495 
496     public class MuleReceiverConnectorHolder extends AbstractConnectorHolder<JettyReceiverServlet, JettyHttpMessageReceiver>
497     {
498         List<MessageReceiver> messageReceivers = new ArrayList<MessageReceiver>();
499 
500         public MuleReceiverConnectorHolder(Connector connector, JettyReceiverServlet servlet, JettyHttpMessageReceiver receiver)
501         {
502             super(connector, servlet, receiver);
503             addReceiver(receiver);
504         }
505 
506         public boolean isReferenced()
507         {
508             return messageReceivers.size() > 0;
509         }
510 
511         public void addReceiver(JettyHttpMessageReceiver receiver)
512         {
513             messageReceivers.add(receiver);
514             if(started)
515             {
516                 getServlet().addReceiver(receiver);
517             }
518         }
519 
520         public void removeReceiver(JettyHttpMessageReceiver receiver)
521         {
522             messageReceivers.remove(receiver);
523             getServlet().removeReceiver(receiver);
524         }
525 
526         @Override
527         public void start() throws MuleException
528         {
529             super.start();
530             
531             for (MessageReceiver receiver : messageReceivers)
532             {
533                 servlet.addReceiver(receiver);
534             }
535         }
536 
537         @Override
538         public void stop() throws MuleException
539         {
540             super.stop();
541 
542             for (MessageReceiver receiver : messageReceivers)
543             {
544                 servlet.removeReceiver(receiver);
545             }
546         }
547     }
548 
549     public String getResourceBase()
550     {
551         return resourceBase;
552     }
553 
554     public void setResourceBase(String resourceBase)
555     {
556         this.resourceBase = resourceBase;
557     }
558 
559     public WebappsConfiguration getWebappsConfiguration()
560     {
561         return webappsConfiguration;
562     }
563 
564     public void setWebappsConfiguration(WebappsConfiguration webappsConfiguration)
565     {
566         this.webappsConfiguration = webappsConfiguration;
567     }
568 
569     /**
570      * A helper method to differentiate between jetty-based connectors which can host full wars and ones which can't.
571      */
572     public boolean canHostFullWars()
573     {
574         return true;
575     }
576 
577     /**
578      * A helper class to let jetty startup, we don't bind java ee objects like java:comp/UserTransaction.
579      */
580     public static class DummyJndiConfiguration extends Configuration
581     {
582 
583         public DummyJndiConfiguration() throws ClassNotFoundException
584         {
585         }
586 
587         @Override
588         public void bindUserTransaction() throws Exception
589         {
590             // no-op
591         }
592 
593         @Override
594         protected void lockCompEnv() throws Exception
595         {
596             // no-op
597         }
598     }
599 }