View Javadoc

1   package org.apache.tomcat.maven.runner;
2   /*
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   */
20  
21  import org.apache.catalina.Context;
22  import org.apache.catalina.Host;
23  import org.apache.catalina.connector.Connector;
24  import org.apache.catalina.core.StandardContext;
25  import org.apache.catalina.startup.Catalina;
26  import org.apache.catalina.startup.ContextConfig;
27  import org.apache.catalina.startup.Tomcat;
28  import org.apache.catalina.valves.AccessLogValve;
29  import org.apache.tomcat.util.http.fileupload.FileUtils;
30  import org.apache.tomcat.util.http.fileupload.IOUtils;
31  
32  import java.io.BufferedOutputStream;
33  import java.io.File;
34  import java.io.FileInputStream;
35  import java.io.FileNotFoundException;
36  import java.io.FileOutputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.lang.reflect.InvocationTargetException;
40  import java.lang.reflect.Method;
41  import java.net.URL;
42  import java.util.HashMap;
43  import java.util.Map;
44  import java.util.Properties;
45  import java.util.StringTokenizer;
46  
47  /**
48   * FIXME add junit for that but when https://issues.apache.org/bugzilla/show_bug.cgi?id=52028 fixed
49   * Main class used to run the standalone wars in a Apache Tomcat instance.
50   *
51   * @author Olivier Lamy
52   * @since 2.0
53   */
54  public class Tomcat7Runner
55  {
56      // true/false to use the server.xml located in the jar /conf/server.xml
57      public static final String USE_SERVER_XML_KEY = "useServerXml";
58  
59      // contains war name wars=foo.war,bar.war
60      public static final String WARS_KEY = "wars";
61  
62      public static final String ARCHIVE_GENERATION_TIMESTAMP_KEY = "generationTimestamp";
63  
64      public static final String ENABLE_NAMING_KEY = "enableNaming";
65  
66      public static final String ACCESS_LOG_VALVE_FORMAT_KEY = "accessLogValveFormat";
67  
68      /**
69       * key of the property which contains http protocol : HTTP/1.1 or org.apache.coyote.http11.Http11NioProtocol
70       */
71      public static final String HTTP_PROTOCOL_KEY = "connectorhttpProtocol";
72  
73  
74      public int httpPort;
75  
76      public int httpsPort;
77  
78      public int ajpPort;
79  
80      public String serverXmlPath;
81  
82      public Properties runtimeProperties;
83  
84      public boolean resetExtract;
85  
86      public boolean debug = false;
87  
88      public boolean clientAuth = false;
89  
90      public String keyAlias = null;
91  
92      public String httpProtocol;
93  
94      public String extractDirectory = ".extract";
95  
96      public File extractDirectoryFile;
97  
98      public String loggerName;
99  
100     Catalina container;
101 
102     Tomcat tomcat;
103 
104     String uriEncoding = "ISO-8859-1";
105 
106     /**
107      * key = context of the webapp, value = war path on file system
108      */
109     Map<String, String> webappWarPerContext = new HashMap<String, String>();
110 
111     public Tomcat7Runner()
112     {
113         // no op
114     }
115 
116     public void run()
117         throws Exception
118     {
119 
120         PasswordUtil.deobfuscateSystemProps();
121 
122         if ( loggerName != null && loggerName.length() > 0 )
123         {
124             installLogger( loggerName );
125         }
126 
127         this.extractDirectoryFile = new File( this.extractDirectory );
128 
129         debugMessage( "use extractDirectory:" + extractDirectoryFile.getPath() );
130 
131         boolean archiveTimestampChanged = false;
132 
133         // compare timestamp stored during previous run if exists
134         File timestampFile = new File( extractDirectoryFile, ".tomcat_executable_archive.timestamp" );
135 
136         Properties timestampProps = loadProperties( timestampFile );
137 
138         if ( timestampFile.exists() )
139         {
140             String timestampValue = timestampProps.getProperty( Tomcat7Runner.ARCHIVE_GENERATION_TIMESTAMP_KEY );
141             if ( timestampValue != null )
142             {
143                 long timestamp = Long.parseLong( timestampValue );
144                 archiveTimestampChanged =
145                     Long.parseLong( runtimeProperties.getProperty( Tomcat7Runner.ARCHIVE_GENERATION_TIMESTAMP_KEY ) )
146                         > timestamp;
147 
148                 debugMessage( "read timestamp from file " + timestampValue + ", archiveTimestampChanged: "
149                                   + archiveTimestampChanged );
150             }
151 
152         }
153 
154         // do we have to extract content
155         {
156             if ( !extractDirectoryFile.exists() || resetExtract || archiveTimestampChanged )
157             {
158                 extract();
159                 //if archiveTimestampChanged or timestamp file not exists store the last timestamp from the archive
160                 if ( archiveTimestampChanged || !timestampFile.exists() )
161                 {
162                     timestampProps.put( Tomcat7Runner.ARCHIVE_GENERATION_TIMESTAMP_KEY, runtimeProperties.getProperty(
163                         Tomcat7Runner.ARCHIVE_GENERATION_TIMESTAMP_KEY ) );
164                     saveProperties( timestampProps, timestampFile );
165                 }
166             }
167             else
168             {
169                 String wars = runtimeProperties.getProperty( WARS_KEY );
170                 populateWebAppWarPerContext( wars );
171             }
172         }
173 
174         // create tomcat various paths
175         new File( extractDirectory, "conf" ).mkdirs();
176         new File( extractDirectory, "logs" ).mkdirs();
177         new File( extractDirectory, "webapps" ).mkdirs();
178         new File( extractDirectory, "work" ).mkdirs();
179         File tmpDir = new File( extractDirectory, "temp" );
180         tmpDir.mkdirs();
181 
182         System.setProperty( "java.io.tmpdir", tmpDir.getAbsolutePath() );
183 
184         System.setProperty( "catalina.base", extractDirectoryFile.getAbsolutePath() );
185         System.setProperty( "catalina.home", extractDirectoryFile.getAbsolutePath() );
186 
187         // start with a server.xml
188         if ( serverXmlPath != null || useServerXml() )
189         {
190             container = new Catalina();
191             container.setUseNaming( this.enableNaming() );
192             if ( serverXmlPath != null && new File( serverXmlPath ).exists() )
193             {
194                 container.setConfig( serverXmlPath );
195             }
196             else
197             {
198                 container.setConfig( new File( extractDirectory, "conf/server.xml" ).getAbsolutePath() );
199             }
200             container.start();
201         }
202         else
203         {
204             tomcat = new Tomcat()
205             {
206                 public Context addWebapp( Host host, String url, String name, String path )
207                 {
208 
209                     Context ctx = new StandardContext();
210                     ctx.setName( name );
211                     ctx.setPath( url );
212                     ctx.setDocBase( path );
213 
214                     ContextConfig ctxCfg = new ContextConfig();
215                     ctx.addLifecycleListener( ctxCfg );
216 
217                     ctxCfg.setDefaultWebXml( new File( extractDirectory, "conf/web.xml" ).getAbsolutePath() );
218 
219                     if ( host == null )
220                     {
221                         getHost().addChild( ctx );
222                     }
223                     else
224                     {
225                         host.addChild( ctx );
226                     }
227 
228                     return ctx;
229                 }
230             };
231 
232             if ( this.enableNaming() )
233             {
234                 System.setProperty( "catalina.useNaming", "true" );
235                 tomcat.enableNaming();
236             }
237 
238             tomcat.getHost().setAppBase( new File( extractDirectory, "webapps" ).getAbsolutePath() );
239 
240             String connectorHttpProtocol = runtimeProperties.getProperty( HTTP_PROTOCOL_KEY );
241 
242             if ( httpProtocol != null && httpProtocol.trim().length() > 0 )
243             {
244                 connectorHttpProtocol = httpProtocol;
245             }
246 
247             debugMessage( "use connectorHttpProtocol:" + connectorHttpProtocol );
248 
249             if ( httpPort > 0 )
250             {
251                 Connector connector = new Connector( connectorHttpProtocol );
252                 connector.setPort( httpPort );
253 
254                 if ( httpsPort > 0 )
255                 {
256                     connector.setRedirectPort( httpsPort );
257                 }
258                 connector.setURIEncoding( uriEncoding );
259 
260                 tomcat.getService().addConnector( connector );
261 
262                 tomcat.setConnector( connector );
263             }
264 
265             // add a default acces log valve
266             AccessLogValve alv = new AccessLogValve();
267             alv.setDirectory( new File( extractDirectory, "logs" ).getAbsolutePath() );
268             alv.setPattern( runtimeProperties.getProperty( Tomcat7Runner.ACCESS_LOG_VALVE_FORMAT_KEY ) );
269             tomcat.getHost().getPipeline().addValve( alv );
270 
271             // create https connector
272             if ( httpsPort > 0 )
273             {
274                 Connector httpsConnector = new Connector( connectorHttpProtocol );
275                 httpsConnector.setPort( httpsPort );
276                 httpsConnector.setSecure( true );
277                 httpsConnector.setProperty( "SSLEnabled", "true" );
278                 httpsConnector.setProperty( "sslProtocol", "TLS" );
279                 httpsConnector.setURIEncoding( uriEncoding );
280 
281                 String keystoreFile = System.getProperty( "javax.net.ssl.keyStore" );
282                 String keystorePass = System.getProperty( "javax.net.ssl.keyStorePassword" );
283                 String keystoreType = System.getProperty( "javax.net.ssl.keyStoreType", "jks" );
284 
285                 if ( keystoreFile != null )
286                 {
287                     httpsConnector.setAttribute( "keystoreFile", keystoreFile );
288                 }
289                 if ( keystorePass != null )
290                 {
291                     httpsConnector.setAttribute( "keystorePass", keystorePass );
292                 }
293                 httpsConnector.setAttribute( "keystoreType", keystoreType );
294 
295                 String truststoreFile = System.getProperty( "javax.net.ssl.trustStore" );
296                 String truststorePass = System.getProperty( "javax.net.ssl.trustStorePassword" );
297                 String truststoreType = System.getProperty( "javax.net.ssl.trustStoreType", "jks" );
298                 if ( truststoreFile != null )
299                 {
300                     httpsConnector.setAttribute( "truststoreFile", truststoreFile );
301                 }
302                 if ( truststorePass != null )
303                 {
304                     httpsConnector.setAttribute( "truststorePass", truststorePass );
305                 }
306                 httpsConnector.setAttribute( "truststoreType", truststoreType );
307 
308                 httpsConnector.setAttribute( "clientAuth", clientAuth );
309                 httpsConnector.setAttribute( "keyAlias", keyAlias );
310 
311                 tomcat.getService().addConnector( httpsConnector );
312 
313                 if ( httpPort <= 0 )
314                 {
315                     tomcat.setConnector( httpsConnector );
316                 }
317             }
318 
319             // create ajp connector
320             if ( ajpPort > 0 )
321             {
322                 Connector ajpConnector = new Connector( "org.apache.coyote.ajp.AjpProtocol" );
323                 ajpConnector.setPort( ajpPort );
324                 ajpConnector.setURIEncoding( uriEncoding );
325                 tomcat.getService().addConnector( ajpConnector );
326             }
327 
328             // add webapps
329             for ( Map.Entry<String, String> entry : this.webappWarPerContext.entrySet() )
330             {
331                 String baseDir = null;
332                 Context context = null;
333                 if ( entry.getKey().equals( "/" ) )
334                 {
335                     baseDir = new File( extractDirectory, "webapps/ROOT.war" ).getAbsolutePath();
336                     context = tomcat.addWebapp( "", baseDir );
337                 }
338                 else
339                 {
340                     baseDir = new File( extractDirectory, "webapps/" + entry.getValue() ).getAbsolutePath();
341                     context = tomcat.addWebapp( entry.getKey(), baseDir );
342                 }
343 
344                 URL contextFileUrl = getContextXml( baseDir );
345                 if ( contextFileUrl != null )
346                 {
347                     context.setConfigFile( contextFileUrl );
348                 }
349             }
350 
351             tomcat.start();
352         }
353 
354         waitIndefinitely();
355 
356     }
357 
358     private URL getContextXml( String warPath )
359         throws IOException
360     {
361         InputStream inputStream = null;
362         try
363         {
364             String urlStr = "jar:file:" + warPath + "!/META-INF/context.xml";
365             debugMessage( "search context.xml in url:'" + urlStr + "'" );
366             URL url = new URL( urlStr );
367             inputStream = url.openConnection().getInputStream();
368             if ( inputStream != null )
369             {
370                 return url;
371             }
372         }
373         catch ( FileNotFoundException e )
374         {
375             return null;
376         }
377         finally
378         {
379             IOUtils.closeQuietly( inputStream );
380         }
381         return null;
382     }
383 
384     private void waitIndefinitely()
385     {
386         Object lock = new Object();
387 
388         synchronized ( lock )
389         {
390             try
391             {
392                 lock.wait();
393             }
394             catch ( InterruptedException exception )
395             {
396                 throw new Error( "InterruptedException on wait Indefinitely lock:" + exception.getMessage(),
397                                  exception );
398             }
399         }
400     }
401 
402     public void stop()
403         throws Exception
404     {
405         if ( container != null )
406         {
407             container.stop();
408         }
409         if ( tomcat != null )
410         {
411             tomcat.stop();
412         }
413     }
414 
415     protected void extract()
416         throws Exception
417     {
418 
419         if ( extractDirectoryFile.exists() )
420         {
421             debugMessage( "delete extractDirectory:" + extractDirectoryFile.getAbsolutePath() );
422             FileUtils.deleteDirectory( extractDirectoryFile );
423         }
424 
425         if ( !this.extractDirectoryFile.exists() )
426         {
427             boolean created = this.extractDirectoryFile.mkdirs();
428             if ( !created )
429             {
430                 throw new Exception( "FATAL: impossible to create directory:" + this.extractDirectoryFile.getPath() );
431             }
432         }
433 
434         // ensure webapp dir is here
435         boolean created = new File( extractDirectory, "webapps" ).mkdirs();
436         if ( !created )
437         {
438             throw new Exception(
439                 "FATAL: impossible to create directory:" + this.extractDirectoryFile.getPath() + "/webapps" );
440 
441         }
442 
443         String wars = runtimeProperties.getProperty( WARS_KEY );
444         populateWebAppWarPerContext( wars );
445 
446         for ( Map.Entry<String, String> entry : webappWarPerContext.entrySet() )
447         {
448             debugMessage( "webappWarPerContext entry key/value: " + entry.getKey() + "/" + entry.getValue() );
449             InputStream inputStream = null;
450             try
451             {
452                 inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( entry.getValue() );
453                 if ( !useServerXml() )
454                 {
455                     if ( entry.getKey().equals( "/" ) )
456                     {
457                         File expandFile = new File( extractDirectory, "webapps/ROOT.war" );
458                         debugMessage( "expand to file:" + expandFile.getPath() );
459                         expand( inputStream, expandFile );
460                     }
461                     else
462                     {
463                         File expandFile = new File( extractDirectory, "webapps/" + entry.getValue() );
464                         debugMessage( "expand to file:" + expandFile.getPath() );
465                         expand( inputStream, expandFile );
466                     }
467                 }
468                 else
469                 {
470                     File expandFile = new File( extractDirectory, "webapps/" + entry.getValue() );
471                     debugMessage( "expand to file:" + expandFile.getPath() );
472                     expand( inputStream, new File( extractDirectory, "webapps/" + entry.getValue() ) );
473                 }
474             }
475             finally
476             {
477                 if ( inputStream != null )
478                 {
479                     inputStream.close();
480                 }
481             }
482         }
483 
484         // expand tomcat configuration files if there
485         expandConfigurationFile( "catalina.properties", extractDirectoryFile );
486         expandConfigurationFile( "logging.properties", extractDirectoryFile );
487         expandConfigurationFile( "tomcat-users.xml", extractDirectoryFile );
488         expandConfigurationFile( "catalina.policy", extractDirectoryFile );
489         expandConfigurationFile( "context.xml", extractDirectoryFile );
490         expandConfigurationFile( "server.xml", extractDirectoryFile );
491         expandConfigurationFile( "web.xml", extractDirectoryFile );
492 
493     }
494 
495     private static void expandConfigurationFile( String fileName, File extractDirectory )
496         throws Exception
497     {
498         InputStream inputStream = null;
499         try
500         {
501             inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( "conf/" + fileName );
502             if ( inputStream != null )
503             {
504                 File confDirectory = new File( extractDirectory, "conf" );
505                 if ( !confDirectory.exists() )
506                 {
507                     confDirectory.mkdirs();
508                 }
509                 expand( inputStream, new File( confDirectory, fileName ) );
510             }
511         }
512         finally
513         {
514             if ( inputStream != null )
515             {
516                 inputStream.close();
517             }
518         }
519 
520     }
521 
522     /**
523      * @param warsValue we can value in format: wars=foo.war|contextpath;bar.war  ( |contextpath is optionnal if empty use the war name)
524      *                  so here we return war file name and populate webappWarPerContext
525      */
526     private void populateWebAppWarPerContext( String warsValue )
527     {
528         StringTokenizer st = new StringTokenizer( warsValue, ";" );
529         while ( st.hasMoreTokens() )
530         {
531             String warValue = st.nextToken();
532             debugMessage( "populateWebAppWarPerContext warValue:" + warValue );
533             String warFileName = "";
534             String contextValue = "";
535             int separatorIndex = warValue.indexOf( "|" );
536             if ( separatorIndex >= 0 )
537             {
538                 warFileName = warValue.substring( 0, separatorIndex );
539                 contextValue = warValue.substring( separatorIndex + 1, warValue.length() );
540 
541             }
542             else
543             {
544                 warFileName = contextValue;
545             }
546             debugMessage( "populateWebAppWarPerContext contextValue/warFileName:" + contextValue + "/" + warFileName );
547             this.webappWarPerContext.put( contextValue, warFileName );
548         }
549     }
550 
551 
552     /**
553      * Expand the specified input stream into the specified file.
554      *
555      * @param input InputStream to be copied
556      * @param file  The file to be created
557      * @throws java.io.IOException if an input/output error occurs
558      */
559     private static void expand( InputStream input, File file )
560         throws IOException
561     {
562         BufferedOutputStream output = null;
563         try
564         {
565             output = new BufferedOutputStream( new FileOutputStream( file ) );
566             byte buffer[] = new byte[2048];
567             while ( true )
568             {
569                 int n = input.read( buffer );
570                 if ( n <= 0 )
571                 {
572                     break;
573                 }
574                 output.write( buffer, 0, n );
575             }
576         }
577         finally
578         {
579             if ( output != null )
580             {
581                 try
582                 {
583                     output.close();
584                 }
585                 catch ( IOException e )
586                 {
587                     // Ignore
588                 }
589             }
590         }
591     }
592 
593     public boolean useServerXml()
594     {
595         return Boolean.parseBoolean( runtimeProperties.getProperty( USE_SERVER_XML_KEY, Boolean.FALSE.toString() ) );
596     }
597 
598 
599     public void debugMessage( String message )
600     {
601         if ( debug )
602         {
603             System.out.println( message );
604         }
605     }
606 
607 
608     public boolean enableNaming()
609     {
610         return Boolean.parseBoolean( runtimeProperties.getProperty( ENABLE_NAMING_KEY, Boolean.FALSE.toString() ) );
611     }
612 
613     private void installLogger( String loggerName )
614         throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
615         InvocationTargetException
616     {
617         if ( "slf4j".equals( loggerName ) )
618         {
619 
620             try
621             {
622                 // Check class is available
623 
624                 //final Class<?> clazz = Class.forName( "org.slf4j.bridge.SLF4JBridgeHandler" );
625                 final Class<?> clazz =
626                     Thread.currentThread().getContextClassLoader().loadClass( "org.slf4j.bridge.SLF4JBridgeHandler" );
627 
628                 // Remove all JUL handlers
629                 java.util.logging.LogManager.getLogManager().reset();
630 
631                 // Install slf4j bridge handler
632                 final Method method = clazz.getMethod( "install", null );
633                 method.invoke( null );
634             }
635             catch ( ClassNotFoundException e )
636             {
637                 System.out.println( "WARNING: issue configuring slf4j jul bridge, skip it" );
638             }
639         }
640         else
641         {
642             System.out.println( "WARNING: loggerName " + loggerName + " not supported, skip it" );
643         }
644     }
645 
646     private Properties loadProperties( File file )
647         throws FileNotFoundException, IOException
648     {
649         Properties properties = new Properties();
650         if ( file.exists() )
651         {
652 
653             FileInputStream fileInputStream = new FileInputStream( file );
654             try
655             {
656                 properties.load( fileInputStream );
657             }
658             finally
659             {
660                 fileInputStream.close();
661             }
662 
663         }
664         return properties;
665     }
666 
667     private void saveProperties( Properties properties, File file )
668         throws FileNotFoundException, IOException
669     {
670         FileOutputStream fileOutputStream = new FileOutputStream( file );
671         try
672         {
673             properties.store( fileOutputStream, "Timestamp file for executable war/jar" );
674         }
675         finally
676         {
677             fileOutputStream.close();
678         }
679     }
680 }