View Javadoc
1   /**
2    *
3    * Copyright 2018 The Apache Software Foundation
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    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver.jar;
18  
19  import org.apache.commons.compress.parallel.InputStreamSupplier;
20  import org.codehaus.plexus.archiver.ArchiverException;
21  import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.PrintStream;
26  import java.nio.file.Files;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.regex.Pattern;
30  
31  /**
32   * A {@link ModularJarArchiver} implementation that uses
33   * the {@code jar} tool provided by
34   * {@code java.util.spi.ToolProvider} to create
35   * modular JAR files.
36   *
37   * <p>
38   * The basic JAR archive is created by {@link JarArchiver}
39   * and the {@code jar} tool is used to upgrade it to modular JAR.
40   *
41   * <p>
42   * If the JAR file does not contain module descriptor
43   * or the JDK does not provide the {@code jar} tool
44   * (for example JDK prior to Java 9), then the
45   * archive created by {@link JarArchiver}
46   * is left unchanged.
47   */
48  public class JarToolModularJarArchiver
49      extends ModularJarArchiver
50  {
51      private static final String MODULE_DESCRIPTOR_FILE_NAME
52          = "module-info.class";
53  
54      private static final Pattern MRJAR_VERSION_AREA
55          = Pattern.compile( "META-INF/versions/\\d+/" );
56  
57      private Object jarTool;
58  
59      private boolean moduleDescriptorFound;
60  
61      public JarToolModularJarArchiver()
62      {
63          try
64          {
65              Class<?> toolProviderClass =
66                  Class.forName( "java.util.spi.ToolProvider" );
67              Object jarToolOptional = toolProviderClass
68                  .getMethod( "findFirst", String.class )
69                  .invoke( null, "jar" );
70  
71              jarTool = jarToolOptional.getClass().getMethod( "get" )
72                  .invoke( jarToolOptional );
73          }
74          catch ( ReflectiveOperationException | SecurityException e )
75          {
76              // Ignore. It is expected that the jar tool
77              // may not be available.
78          }
79      }
80  
81      @Override
82      protected void zipFile( InputStreamSupplier is, ConcurrentJarCreator zOut,
83                              String vPath, long lastModified, File fromArchive,
84                              int mode, String symlinkDestination,
85                              boolean addInParallel )
86          throws IOException, ArchiverException
87      {
88          if ( jarTool != null && isModuleDescriptor( vPath ) )
89          {
90              getLogger().debug( "Module descriptor found: " + vPath );
91  
92              moduleDescriptorFound = true;
93          }
94  
95          super.zipFile( is, zOut, vPath, lastModified,
96              fromArchive, mode, symlinkDestination, addInParallel );
97      }
98  
99      @Override
100     protected void postCreateArchive()
101         throws ArchiverException
102     {
103         if ( !moduleDescriptorFound )
104         {
105             // no need to update the JAR archive
106             return;
107         }
108 
109         try
110         {
111             getLogger().debug( "Using the jar tool to " +
112                 "update the archive to modular JAR." );
113 
114 			Integer result = (Integer) jarTool.getClass()
115                 .getMethod( "run",
116                     PrintStream.class, PrintStream.class, String[].class )
117                 .invoke( jarTool,
118                     System.out, System.err,
119                     getJarToolArguments() );
120 
121             if ( result != null && result != 0 )
122             {
123                 throw new ArchiverException( "Could not create modular JAR file. " +
124                     "The JDK jar tool exited with " + result );
125             }
126         }
127         catch ( IOException | ReflectiveOperationException | SecurityException e )
128         {
129             throw new ArchiverException( "Exception occurred " +
130                 "while creating modular JAR file", e );
131         }
132     }
133 
134     /**
135      * Returns {@code true} if {@code path}
136      * is a module descriptor.
137      */
138     private boolean isModuleDescriptor( String path )
139     {
140         if ( path.endsWith( MODULE_DESCRIPTOR_FILE_NAME ) )
141         {
142             String prefix = path.substring( 0,
143                 path.lastIndexOf( MODULE_DESCRIPTOR_FILE_NAME ) );
144 
145             // the path is a module descriptor if it located
146             // into the root of the archive or into the
147             // version area of a multi-release JAR file
148             return prefix.isEmpty() ||
149                 MRJAR_VERSION_AREA.matcher( prefix ).matches();
150         }
151         else
152         {
153             return false;
154         }
155     }
156 
157     /**
158      * Prepares the arguments for the jar tool.
159      * It takes into account the module version,
160      * main class, etc.
161      */
162     private String[] getJarToolArguments()
163         throws IOException
164     {
165         // We add empty temporary directory to the JAR file.
166         // It may look strange at first, but to update a JAR file
167         // you need to add new files[1]. If we add empty directory
168         // it will be ignored (not added to the archive), but
169         // the module descriptor will be updated and validated.
170         //
171         // [1] There are some exceptions (such as when the main class
172         // is updated) but we need at least empty directory
173         // to ensure it will work in all cases.
174         File tempEmptyDir = Files.createTempDirectory( null ).toFile();
175         tempEmptyDir.deleteOnExit();
176 
177         List<String> args = new ArrayList<>();
178 
179         args.add( "--update" );
180         args.add( "--file" );
181         args.add( getDestFile().getAbsolutePath() );
182 
183         String mainClass = getModuleMainClass() != null
184                            ? getModuleMainClass()
185                            : getManifestMainClass();
186 
187         if ( mainClass != null )
188         {
189             args.add( "--main-class" );
190             args.add( mainClass );
191         }
192 
193         if ( getModuleVersion() != null )
194         {
195             args.add( "--module-version" );
196             args.add( getModuleVersion() );
197         }
198 
199         if ( !isCompress() )
200         {
201             args.add( "--no-compress" );
202         }
203 
204         args.add( "-C" );
205         args.add( tempEmptyDir.getAbsolutePath() );
206         args.add( "." );
207 
208         return args.toArray( new String[]{} );
209     }
210 
211 }