View Javadoc

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