View Javadoc

1   package com.atlassian.plugin;
2   
3   import com.google.common.base.Function;
4   import com.google.common.base.Predicate;
5   import com.google.common.collect.Sets;
6   import org.apache.commons.io.IOUtils;
7   import org.slf4j.Logger;
8   import org.slf4j.LoggerFactory;
9   
10  import javax.annotation.Nullable;
11  import java.io.BufferedInputStream;
12  import java.io.File;
13  import java.io.FileInputStream;
14  import java.io.FileNotFoundException;
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.util.Set;
18  import java.util.jar.JarEntry;
19  import java.util.jar.JarFile;
20  import java.util.jar.Manifest;
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  import java.util.zip.ZipEntry;
24  
25  import static com.google.common.base.Preconditions.checkNotNull;
26  import static com.google.common.collect.Iterators.any;
27  import static com.google.common.collect.Iterators.filter;
28  import static com.google.common.collect.Iterators.forEnumeration;
29  import static com.google.common.collect.Iterators.transform;
30  
31  /**
32   * The implementation of PluginArtifact that is backed by a jar file.
33   *
34   * @see PluginArtifact
35   * @since 2.0.0
36   */
37  public final class JarPluginArtifact implements PluginArtifact, PluginArtifact.AllowsReference, PluginArtifact.HasExtraModuleDescriptors
38  {
39      private static final Logger log = LoggerFactory.getLogger(JarPluginArtifact.class);
40  
41      private final File jarFile;
42  
43      /**
44       * The {@link ReferenceMode} used to implement {@link #allowsReference}.
45       */
46      final ReferenceMode referenceMode;
47  
48      /**
49       * Construct a PluginArtifact for a jar file which does not allow reference installation.
50       * @param jarFile the jar file comprising the artifact.
51       */
52      public JarPluginArtifact(File jarFile)
53      {
54          this(jarFile, ReferenceMode.FORBID_REFERENCE);
55      }
56  
57      /**
58       * Construct a PluginArtifact for a jar file and specify whether reference installation is supported.
59       * @param jarFile the jar file comprising the artifact.
60       * @param referenceMode The {@link ReferenceMode} used to implement {@link #allowsReference}.
61       */
62      public JarPluginArtifact(File jarFile, ReferenceMode referenceMode)
63      {
64          this.jarFile = checkNotNull(jarFile);
65          this.referenceMode = referenceMode;
66      }
67  
68  
69      public boolean doesResourceExist(String name)
70      {
71          InputStream in = null;
72          try
73          {
74              in = getResourceAsStream(name);
75              return (in != null);
76          }
77          finally
78          {
79              IOUtils.closeQuietly(in);
80          }
81      }
82  
83      /**
84       * @return an input stream for the this file in the jar. Closing this stream also closes the jar file this stream comes from.
85       */
86      public InputStream getResourceAsStream(String fileName) throws PluginParseException
87      {
88          checkNotNull(fileName, "The file name must not be null");
89  
90          final JarFile jar = open();
91          final ZipEntry entry = jar.getEntry(fileName);
92          if (entry == null)
93          {
94              closeJarQuietly(jar);
95              return null;
96          }
97  
98          try
99          {
100             return new BufferedInputStream(jar.getInputStream(entry))
101             {
102                 // because we do not expose a handle to the jar file this stream is associated with, we need to make sure
103                 // we explicitly close the jar file when we're done with the stream (else we'll have a file handle leak)
104                 public void close() throws IOException
105                 {
106                     super.close();
107                     jar.close();
108                 }
109             };
110         }
111         catch (IOException e)
112         {
113             throw new PluginParseException("Cannot retrieve " + fileName + " from plugin JAR [" + jarFile + "]", e);
114         }
115     }
116 
117     public String getName()
118     {
119         return jarFile.getName();
120     }
121 
122     @Override
123     public String toString()
124     {
125         return getName();
126     }
127 
128     /**
129      * @return a buffered file input stream of the file on disk. This input stream
130      *         is not resettable.
131      */
132     public InputStream getInputStream()
133     {
134         try
135         {
136             return new BufferedInputStream(new FileInputStream(jarFile));
137         }
138         catch (FileNotFoundException e)
139         {
140             throw new PluginParseException("Could not open JAR file: " + jarFile, e);
141         }
142     }
143 
144     public File toFile()
145     {
146         return jarFile;
147     }
148 
149     @Override
150     public boolean containsJavaExecutableCode()
151     {
152         final JarFile jar = open();
153         try
154         {
155             final Manifest manifest = getManifest(jar);
156             return hasBundleActivator(manifest)
157                     ||
158                     hasSpringContext(manifest)
159                     ||
160                     any(forEnumeration(jar.entries()), new Predicate<JarEntry>()
161                     {
162                         @Override
163                         public boolean apply(JarEntry entry)
164                         {
165                             return isJavaClass(entry) || isJavaLibrary(entry) || isSpringContext(entry);
166                         }
167                     });
168         }
169         finally
170         {
171             closeJarQuietly(jar);
172         }
173     }
174 
175     @Override
176     public Set<String> extraModuleDescriptorFiles(String rootFolder)
177     {
178         final JarFile jar = open();
179         try
180         {
181             final Matcher m = Pattern.compile(Pattern.quote(rootFolder) + "/[^/.]*\\.(?i)xml$").matcher("");
182             return Sets.newHashSet(transform(filter(forEnumeration(jar.entries()),
183                     new Predicate<JarEntry>()
184                     {
185                         @Override
186                         public boolean apply(JarEntry entry)
187                         {
188                             m.reset(entry.getName());
189                             return m.find();
190                         }
191                 }), new Function<JarEntry, String>()
192                     {
193                         @Override
194                         public String apply(@Nullable JarEntry jarEntry)
195                         {
196                             return jarEntry.getName();
197                         }
198                 }));
199         }
200         finally
201         {
202             closeJarQuietly(jar);
203         }
204     }
205 
206     @Override
207     public boolean allowsReference()
208     {
209         return referenceMode.allowsReference();
210     }
211 
212     private boolean isJavaClass(ZipEntry entry)
213     {
214         return entry.getName().endsWith(".class");
215     }
216 
217     private boolean isJavaLibrary(ZipEntry entry)
218     {
219         return entry.getName().endsWith(".jar");
220     }
221 
222     private boolean isSpringContext(ZipEntry entry)
223     {
224         final String entryName = entry.getName();
225         return entryName.startsWith("META-INF/spring/") && entryName.endsWith(".xml");
226     }
227 
228     private boolean hasSpringContext(Manifest manifest)
229     {
230         return hasManifestEntry(manifest, "Spring-Context");
231     }
232 
233     private boolean hasBundleActivator(Manifest manifest)
234     {
235         return hasManifestEntry(manifest, "Bundle-Activator");
236     }
237 
238     private boolean hasManifestEntry(Manifest manifest, String manifestEntryName)
239     {
240         return manifest != null
241                 && manifest.getMainAttributes() != null
242                 && manifest.getMainAttributes().getValue(manifestEntryName) != null;
243     }
244 
245     private JarFile open()
246     {
247         try
248         {
249             return new JarFile(jarFile);
250         }
251         catch (IOException e)
252         {
253             throw new PluginParseException("Cannot open JAR file: " + jarFile, e);
254         }
255     }
256 
257     private Manifest getManifest(JarFile jar)
258     {
259         try
260         {
261             return jar.getManifest();
262         }
263         catch (IOException e)
264         {
265             throw new PluginParseException("Cannot get manifest for JAR file: " + jarFile, e);
266         }
267     }
268 
269     private void closeJarQuietly(JarFile jar)
270     {
271         if (jar != null)
272         {
273             try
274             {
275                 jar.close();
276             }
277             catch (IOException ignored)
278             {
279                 log.debug("Exception closing jar file " + jarFile + ".", ignored);
280             }
281         }
282     }
283 }