Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: bfc8cabea7eb0f1f2c3c01e6b0277870909f4891 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
//
//  ========================================================================
//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.jspc.plugin;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.jasper.JspC;
import org.apache.jasper.servlet.JspCServletContext;
import org.apache.jasper.servlet.TldScanner;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.util.scan.StandardJarScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.Resource;

/**
 * This goal will compile jsps for a webapp so that they can be included in a
 * war.
 * <p>
 * At runtime, the plugin will use the jspc compiler to precompile jsps and tags.
 * </p>
 * <p>
 * Note that the same java compiler will be used as for on-the-fly compiled
 * jsps, which will be the Eclipse java compiler.
 * <p>
 * See <a
 * href="https://www.eclipse.org/jetty/documentation/current/jetty-jspc-maven-plugin.html">Usage
 * Guide</a> for instructions on using this plugin.
 * </p>
 * @goal jspc
 * @phase process-classes
 * @requiresDependencyResolution compile+runtime
 * @description Runs jspc compiler to produce .java and .class files
 */
public class JspcMojo extends AbstractMojo
{
    public static final String END_OF_WEBAPP = "</web-app>";
    public static final String PRECOMPILED_FLAG = "org.eclipse.jetty.jsp.precompiled";


    /**
     * JettyJspC
     *
     * Add some extra setters to standard JspC class to help configure it
     * for running in maven.
     * 
     * TODO move all setters on the plugin onto this jspc class instead.
     */
    public static class JettyJspC extends JspC
    {
   
        private boolean scanAll;
        
        public void setClassLoader (ClassLoader loader)
        {
            this.loader = loader;
        }
        
       public void setScanAllDirectories (boolean scanAll)
       {
           this.scanAll = scanAll;
       }
       
       public boolean getScanAllDirectories ()
       {
           return this.scanAll;
       }
       

        @Override
        protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
        {            
            if (context != null && context.getAttribute(JarScanner.class.getName()) == null) 
            {
                StandardJarScanner jarScanner = new StandardJarScanner();             
                jarScanner.setScanAllDirectories(getScanAllDirectories());
                context.setAttribute(JarScanner.class.getName(), jarScanner);
            }
                
            return super.newTldScanner(context, namespaceAware, validate, blockExternal);
        }      
    }
    
    
    /**
     * Whether or not to include dependencies on the plugin's classpath with &lt;scope&gt;provided&lt;/scope&gt;
     * Use WITH CAUTION as you may wind up with duplicate jars/classes.
     * 
     * @since jetty-7.6.3
     * @parameter  default-value="false"
     */
    private boolean useProvidedScope;
    
    /**
     * The artifacts for the project.
     * 
     * @since jetty-7.6.3
     * @parameter default-value="${project.artifacts}"
     * @readonly
     */
    private Set projectArtifacts;
    
    
    /**
     * The maven project.
     * 
     * @parameter default-value="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    

    /**
     * The artifacts for the plugin itself.
     * 
     * @parameter default-value="${plugin.artifacts}"
     * @readonly
     */
    private List pluginArtifacts;
    
    
    /**
     * File into which to generate the &lt;servlet&gt; and
     * &lt;servlet-mapping&gt; tags for the compiled jsps
     * 
     * @parameter default-value="${basedir}/target/webfrag.xml"
     */
    private String webXmlFragment;

    /**
     * Optional. A marker string in the src web.xml file which indicates where
     * to merge in the generated web.xml fragment. Note that the marker string
     * will NOT be preserved during the insertion. Can be left blank, in which
     * case the generated fragment is inserted just before the &lt;/web-app&gt;
     * line
     * 
     * @parameter
     */
    private String insertionMarker;

    /**
     * Merge the generated fragment file with the web.xml from
     * webAppSourceDirectory. The merged file will go into the same directory as
     * the webXmlFragment.
     * 
     * @parameter default-value="true"
     */
    private boolean mergeFragment;

    /**
     * The destination directory into which to put the compiled jsps.
     * 
     * @parameter default-value="${project.build.outputDirectory}"
     */
    private String generatedClasses;

    /**
     * Controls whether or not .java files generated during compilation will be
     * preserved.
     * 
     * @parameter default-value="false"
     */
    private boolean keepSources;


    /**
     * Root directory for all html/jsp etc files
     * 
     * @parameter default-value="${basedir}/src/main/webapp"
     * 
     */
    private String webAppSourceDirectory;
    
   
    
    /**
     * Location of web.xml. Defaults to src/main/webapp/web.xml.
     * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/web.xml"
     */
    private String webXml;


    /**
     * The comma separated list of patterns for file extensions to be processed. By default
     * will include all .jsp and .jspx files.
     * 
     * @parameter default-value="**\/*.jsp, **\/*.jspx"
     */
    private String includes;

    /**
     * The comma separated list of file name patters to exclude from compilation.
     * 
     * @parameter default_value="**\/.svn\/**";
     */
    private String excludes;

    /**
     * The location of the compiled classes for the webapp
     * 
     * @parameter default-value="${project.build.outputDirectory}"
     */
    private File classesDirectory;

    
    /**
     * Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
     * 
     * @parameter default-value=".*taglibs[^/]*\.jar|.*jstl[^/]*\.jar$
     */
    private String tldJarNamePatterns;
    
    
    /**
     * Source version - if not set defaults to jsp default (currently 1.7)
     * @parameter 
     */
    private String sourceVersion;
    
    
    /**
     * Target version - if not set defaults to jsp default (currently 1.7)
     * @parameter 
     */
    private String targetVersion;
    
    /**
     * 
     * The JspC instance being used to compile the jsps.
     * 
     * @parameter
     */
    private JettyJspC jspc;


    /**
     * Whether dirs on the classpath should be scanned as well as jars.
     * True by default. This allows for scanning for tlds of dependent projects that
     * are in the reactor as unassembled jars.
     * 
     * @parameter default-value=true
     */
    private boolean scanAllDirectories;
    

    public void execute() throws MojoExecutionException, MojoFailureException
    {
        if (getLog().isDebugEnabled())
        {

            getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
            getLog().info("generatedClasses=" + generatedClasses);
            getLog().info("webXmlFragment=" + webXmlFragment);
            getLog().info("webXml="+webXml);
            getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
            getLog().info("keepSources=" + keepSources);
            getLog().info("mergeFragment=" + mergeFragment);  
            if (sourceVersion != null)
                getLog().info("sourceVersion="+sourceVersion);
            if (targetVersion != null)
                getLog().info("targetVersion="+targetVersion);
        }
        try
        {
            prepare();
            compile();
            cleanupSrcs();
            mergeWebXml();
        }
        catch (Exception e)
        {
            throw new MojoExecutionException("Failure processing jsps", e);
        }
    }

    public void compile() throws Exception
    {
        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();

        //set up the classpath of the webapp
        List<URL> webAppUrls = setUpWebAppClassPath();
        
        //set up the classpath of the container (ie jetty and jsp jars)
        Set<URL> pluginJars = getPluginJars();
        Set<URL> providedJars = getProvidedScopeJars(pluginJars);
 

        //Make a classloader so provided jars will be on the classpath
        List<URL> sysUrls = new ArrayList<URL>();      
        sysUrls.addAll(providedJars);     
        URLClassLoader sysClassLoader = new URLClassLoader((URL[])sysUrls.toArray(new URL[0]), currentClassLoader);
      
        //make a classloader with the webapp classpath
        URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), sysClassLoader);
        StringBuffer webAppClassPath = new StringBuffer();

        for (int i = 0; i < webAppUrls.size(); i++)
        {
            if (getLog().isDebugEnabled())
                getLog().debug("webappclassloader contains: " + webAppUrls.get(i));                
            webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
            if (getLog().isDebugEnabled())
                getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
            if (i+1<webAppUrls.size())
                webAppClassPath.append(System.getProperty("path.separator"));
        }
        
        //Interpose a fake classloader as the webapp class loader. This is because the Apache JspC class
        //uses a TldScanner which ignores jars outside of the WEB-INF/lib path on the webapp classloader.
        //It will, however, look at all jars on the parents of the webapp classloader.
        URLClassLoader fakeWebAppClassLoader = new URLClassLoader(new URL[0], webAppClassLoader);
        Thread.currentThread().setContextClassLoader(fakeWebAppClassLoader);
  
        if (jspc == null)
            jspc = new JettyJspC();
        

        jspc.setWebXmlFragment(webXmlFragment);
        jspc.setUriroot(webAppSourceDirectory);     
        jspc.setOutputDir(generatedClasses);
        jspc.setClassLoader(fakeWebAppClassLoader);
        jspc.setScanAllDirectories(scanAllDirectories);
        jspc.setCompile(true);
        if (sourceVersion != null)
            jspc.setCompilerSourceVM(sourceVersion);
        if (targetVersion != null)
            jspc.setCompilerTargetVM(targetVersion);

        // JspC#setExtensions() does not exist, so 
        // always set concrete list of files that will be processed.
        String jspFiles = getJspFiles(webAppSourceDirectory);
       
        try
        {
            if (jspFiles == null | jspFiles.equals(""))
            {
                getLog().info("No files selected to precompile");
            }
            else
            {
                getLog().info("Compiling "+jspFiles+" from includes="+includes+" excludes="+excludes);
                jspc.setJspFiles(jspFiles);
                jspc.execute();
            }
        }
        finally
        {

            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }

    private String getJspFiles(String webAppSourceDirectory)
    throws Exception
    {
        List fileNames =  FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
        return StringUtils.join(fileNames.toArray(new String[0]), ",");

    }

    /**
     * Until Jasper supports the option to generate the srcs in a different dir
     * than the classes, this is the best we can do.
     * 
     * @throws Exception if unable to clean srcs
     */
    public void cleanupSrcs() throws Exception
    {
        // delete the .java files - depending on keepGenerated setting
        if (!keepSources)
        {
            File generatedClassesDir = new File(generatedClasses);

            if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
            {
                delete(generatedClassesDir, new FileFilter()
                {
                    public boolean accept(File f)
                    {
                        return f.isDirectory() || f.getName().endsWith(".java");
                    }                
                });
            }
        }
    }
    
    static void delete(File dir, FileFilter filter)
    {
        File[] files = dir.listFiles(filter);
        if (files != null)
        {
            for(File f: files)
            {
                if(f.isDirectory())
                    delete(f, filter);
                else
                    f.delete();
            }
        }
    }

    /**
     * Take the web fragment and put it inside a copy of the web.xml.
     * 
     * You can specify the insertion point by specifying the string in the
     * insertionMarker configuration entry.
     * 
     * If you dont specify the insertionMarker, then the fragment will be
     * inserted at the end of the file just before the &lt;/webapp&gt;
     * 
     * @throws Exception if unable to merge the web xml
     */
    public void mergeWebXml() throws Exception
    {
        if (mergeFragment)
        {
            // open the src web.xml
            File webXml = getWebXmlFile();
           
            if (!webXml.exists())
            {
                getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
                return;
            }

            File fragmentWebXml = new File(webXmlFragment);         
            File mergedWebXml = new File(fragmentWebXml.getParentFile(), "web.xml");

            try (BufferedReader webXmlReader = new BufferedReader(new FileReader(webXml));
                 PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(mergedWebXml))) 
            {

                if (!fragmentWebXml.exists())
                {
                    getLog().info("No fragment web.xml file generated");
                    //just copy existing web.xml to expected position
                    IO.copy(webXmlReader, mergedWebXmlWriter);
                }
                else
                {
                    // read up to the insertion marker or the </webapp> if there is no
                    // marker
                    boolean atInsertPoint = false;
                    boolean atEOF = false;
                    String marker = (insertionMarker == null
                            || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
                    while (!atInsertPoint && !atEOF)
                    {
                        String line = webXmlReader.readLine();
                        if (line == null)
                            atEOF = true;
                        else if (line.indexOf(marker) >= 0)
                        {
                            atInsertPoint = true;
                        }
                        else
                        {
                            mergedWebXmlWriter.println(line);
                        }
                    }

                    if (atEOF && !atInsertPoint)
                        throw new IllegalStateException("web.xml does not contain insertionMarker "+insertionMarker);

                    //put in a context init-param to flag that the contents have been precompiled
                    mergedWebXmlWriter.println("<context-param><param-name>"+PRECOMPILED_FLAG+"</param-name><param-value>true</param-value></context-param>");


                    // put in the generated fragment
                    try (BufferedReader fragmentWebXmlReader = 
                            new BufferedReader(new FileReader(fragmentWebXml))) 
                    {
                        IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);

                        // if we inserted just before the </web-app>, put it back in
                        if (marker.equals(END_OF_WEBAPP))
                            mergedWebXmlWriter.println(END_OF_WEBAPP);

                        // copy in the rest of the original web.xml file
                        IO.copy(webXmlReader, mergedWebXmlWriter);
                    }
                }
            }
        }
    }

    private void prepare() throws Exception
    {
        // For some reason JspC doesn't like it if the dir doesn't
        // already exist and refuses to create the web.xml fragment
        File generatedSourceDirectoryFile = new File(generatedClasses);
        if (!generatedSourceDirectoryFile.exists())
            generatedSourceDirectoryFile.mkdirs();
    }

    /**
     * Set up the execution classpath for Jasper.
     * 
     * Put everything in the classesDirectory and all of the dependencies on the
     * classpath.
     * 
     * @returns a list of the urls of the dependencies
     * @throws Exception
     */
    private List<URL> setUpWebAppClassPath() throws Exception
    {
        //add any classes from the webapp
        List<URL> urls = new ArrayList<URL>();
        String classesDir = classesDirectory.getCanonicalPath();
        classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
        urls.add(Resource.toURL(new File(classesDir)));

        if (getLog().isDebugEnabled())
            getLog().debug("Adding to classpath classes dir: " + classesDir);

        //add the dependencies of the webapp (which will form WEB-INF/lib)
        for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
        {
            Artifact artifact = (Artifact)iter.next();

            // Include runtime and compile time libraries
            if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
            {
                String filePath = artifact.getFile().getCanonicalPath();
                if (getLog().isDebugEnabled())
                    getLog().debug("Adding to classpath dependency file: " + filePath);

                urls.add(Resource.toURL(artifact.getFile()));
            }
        }
        return urls;
    }
    
    
    
    /**
     * @return
     * @throws MalformedURLException
     */
    private Set<URL> getPluginJars () throws MalformedURLException
    {
        HashSet<URL> pluginJars = new HashSet<>();
        for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
        {
            Artifact pluginArtifact = iter.next();
            if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
            {
                if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
                pluginJars.add(pluginArtifact.getFile().toURI().toURL());
            }
        }
        
        return pluginJars;
    }
    
    
    
    /**
     * @param pluginJars
     * @return
     * @throws MalformedURLException
     */
    private Set<URL>  getProvidedScopeJars (Set<URL> pluginJars) throws MalformedURLException
    {
        if (!useProvidedScope)
            return Collections.emptySet();
        
        HashSet<URL> providedJars = new HashSet<>();
        
        for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
        {                   
            Artifact artifact = iter.next();
            if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
            {
                //test to see if the provided artifact was amongst the plugin artifacts
                URL jar = artifact.getFile().toURI().toURL();
                if (!pluginJars.contains(jar))
                {
                    providedJars.add(jar);
                    if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
                }  
                else
                {
                    if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
                }
            }
        }
        return providedJars;
    }

    
    
    private File getWebXmlFile ()
    throws IOException
    {
        File file = null;
        File baseDir = project.getBasedir().getCanonicalFile();
        File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
        File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
        File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
        
        //If the web.xml has been changed from the default, try that
        File webXmlFile = new File (webXml).getCanonicalFile();
        if (webXmlFile.compareTo(defaultWebXml) != 0)
        {
            file = new File (webXml);
            return file;
        }
        
        //If the web app src directory has not been changed from the default, use whatever
        //is set for the web.xml location
        file = new File (webAppSrcDir, "web.xml");
        return file;
    }
}

Back to the top